- Backend: Update Legacy API credentials to robster/rob2 - Frontend: ROOM_LOCATIONS with icons and legacyCode mapping - Device Settings: Modal picker for room selection - api.ts: Bidirectional conversion (code ↔ name) - Various UI/UX improvements across screens PRD-DEPLOYMENT.md completed (Score: 9/10) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
ActivityIndicator,
|
|
Platform,
|
|
} from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { router, useLocalSearchParams } from 'expo-router';
|
|
// import { usePaymentSheet } from '@stripe/stripe-react-native'; // Removed for web
|
|
import { api } from '@/services/api';
|
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { useToast } from '@/components/ui/Toast';
|
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
|
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
|
import {
|
|
AppColors,
|
|
BorderRadius,
|
|
FontSizes,
|
|
FontWeights,
|
|
Spacing,
|
|
Shadows,
|
|
} from '@/constants/theme';
|
|
import type { Beneficiary } from '@/types';
|
|
import { hasBeneficiaryDevices } from '@/services/BeneficiaryDetailController';
|
|
|
|
// const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe';
|
|
|
|
const STARTER_KIT = {
|
|
name: 'WellNuo Starter Kit',
|
|
price: '$399',
|
|
priceValue: 399,
|
|
description: '5 smart sensors that easily plug into any outlet and set up through the app in minutes',
|
|
};
|
|
|
|
export default function PurchaseScreen() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const { user } = useAuth();
|
|
const toast = useToast();
|
|
|
|
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
|
|
// const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet(); // Removed for web
|
|
|
|
const loadBeneficiary = useCallback(async () => {
|
|
if (!id) return;
|
|
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await api.getWellNuoBeneficiary(parseInt(id, 10));
|
|
if (response.ok && response.data) {
|
|
setBeneficiary(response.data);
|
|
|
|
// Redirect if user already has devices - shouldn't be on purchase page
|
|
if (hasBeneficiaryDevices(response.data)) {
|
|
router.replace(`/(tabs)/beneficiaries/${id}`);
|
|
return;
|
|
}
|
|
|
|
// Redirect if equipment is ordered/shipped/delivered
|
|
const status = response.data.equipmentStatus;
|
|
if (status && ['ordered', 'shipped', 'delivered'].includes(status)) {
|
|
router.replace(`/(tabs)/beneficiaries/${id}/equipment-status`);
|
|
return;
|
|
}
|
|
} else {
|
|
setError(response.error?.message || 'Failed to load beneficiary');
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
loadBeneficiary();
|
|
}, [loadBeneficiary]);
|
|
|
|
const handlePurchase = async () => {
|
|
// Web implementation: Placeholder or redirection to external Stripe checkout if needed.
|
|
// For now, we just inform the user.
|
|
alert('Purchases are currently only available in the mobile app.');
|
|
return;
|
|
};
|
|
|
|
const handleAlreadyHaveSensors = () => {
|
|
router.push({
|
|
pathname: '/(auth)/activate',
|
|
params: { beneficiaryId: id, lovedOneName: beneficiary?.name },
|
|
});
|
|
};
|
|
|
|
if (isLoading) {
|
|
return <LoadingSpinner fullScreen message="Loading..." />;
|
|
}
|
|
|
|
if (error || !beneficiary) {
|
|
return (
|
|
<FullScreenError
|
|
message={error || 'Beneficiary not found'}
|
|
onRetry={loadBeneficiary}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>Get Started</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
<ScrollView
|
|
style={styles.content}
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* Icon */}
|
|
<View style={styles.iconContainer}>
|
|
<Ionicons name="hardware-chip-outline" size={64} color={AppColors.primary} />
|
|
</View>
|
|
|
|
{/* Title */}
|
|
<Text style={styles.title}>Start Monitoring {beneficiary.displayName}</Text>
|
|
<Text style={styles.subtitle}>
|
|
To monitor wellness, you need WellNuo sensors installed in their home.
|
|
</Text>
|
|
|
|
{/* Kit Card */}
|
|
<View style={styles.kitCard}>
|
|
<Text style={styles.kitName}>{STARTER_KIT.name}</Text>
|
|
<Text style={styles.kitPrice}>{STARTER_KIT.price}</Text>
|
|
|
|
<Text style={styles.kitDescription}>{STARTER_KIT.description}</Text>
|
|
|
|
{/* Security Badge */}
|
|
<View style={styles.securityBadge}>
|
|
<Ionicons name="shield-checkmark" size={16} color={AppColors.success} />
|
|
<Text style={styles.securityText}>Secure payment by Stripe</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Actions */}
|
|
<View style={styles.actionsSection}>
|
|
<TouchableOpacity
|
|
style={[styles.buyButton, isProcessing && styles.buyButtonDisabled]}
|
|
onPress={handlePurchase}
|
|
disabled={isProcessing}
|
|
>
|
|
{isProcessing ? (
|
|
<ActivityIndicator color={AppColors.white} />
|
|
) : (
|
|
<>
|
|
<Ionicons name="card" size={20} color={AppColors.white} />
|
|
<Text style={styles.buyButtonText}>Buy Now</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity style={styles.alreadyHaveButton} onPress={handleAlreadyHaveSensors}>
|
|
<Text style={styles.alreadyHaveText}>I already have sensors</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View style={{ marginTop: 20, padding: 10, backgroundColor: '#f0f9ff', borderRadius: 8 }}>
|
|
<Text style={{ textAlign: 'center', color: '#0066cc', fontSize: 14 }}>
|
|
Web version: Payment flow is limited. Please use mobile app for purchases.
|
|
</Text>
|
|
</View>
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: Spacing.md,
|
|
paddingVertical: Spacing.sm,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: AppColors.border,
|
|
},
|
|
backButton: {
|
|
padding: Spacing.xs,
|
|
},
|
|
headerTitle: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
placeholder: {
|
|
width: 32,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
padding: Spacing.lg,
|
|
paddingBottom: Spacing.xxl,
|
|
},
|
|
iconContainer: {
|
|
width: 120,
|
|
height: 120,
|
|
borderRadius: 60,
|
|
backgroundColor: AppColors.primaryLighter,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
alignSelf: 'center',
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
title: {
|
|
fontSize: FontSizes['2xl'],
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
textAlign: 'center',
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
subtitle: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
marginBottom: Spacing.xl,
|
|
lineHeight: 22,
|
|
},
|
|
kitCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
padding: Spacing.lg,
|
|
marginBottom: Spacing.lg,
|
|
...Shadows.md,
|
|
},
|
|
kitName: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
textAlign: 'center',
|
|
},
|
|
kitPrice: {
|
|
fontSize: FontSizes['3xl'],
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.primary,
|
|
textAlign: 'center',
|
|
marginVertical: Spacing.md,
|
|
},
|
|
kitDescription: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
lineHeight: 22,
|
|
},
|
|
actionsSection: {
|
|
gap: Spacing.md,
|
|
},
|
|
buyButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: AppColors.primary,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
gap: Spacing.sm,
|
|
},
|
|
buyButtonDisabled: {
|
|
opacity: 0.7,
|
|
},
|
|
buyButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.white,
|
|
},
|
|
securityBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginTop: Spacing.md,
|
|
gap: Spacing.xs,
|
|
},
|
|
securityText: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.success,
|
|
},
|
|
alreadyHaveButton: {
|
|
alignItems: 'center',
|
|
paddingVertical: Spacing.sm,
|
|
},
|
|
alreadyHaveText: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textDecorationLine: 'underline',
|
|
},
|
|
});
|