WellNuo/components/screens/auth-purchase/PurchaseScreen.web.tsx
Sergei 5e0b38748b Update Stripe integration, API services, and purchase screens
- Update purchase screens (auth and beneficiary)
- Update Stripe configuration and setup scripts
- Update api.ts services
- Update espProvisioning and sherpaTTS services
- Update verify-otp flow
- Package updates
2026-01-12 21:44:57 -08:00

382 lines
13 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
Alert,
Platform,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router, useLocalSearchParams } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
// import { usePaymentSheet } from '@stripe/stripe-react-native'; // Removed for web
import { AppColors, Spacing, BorderRadius, FontSizes, FontWeights, Shadows } from '@/constants/theme';
import { useAuth } from '@/contexts/AuthContext';
import { api } from '@/services/api';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
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,
};
export default function PurchaseScreen() {
const params = useLocalSearchParams<{ lovedOneName?: string; beneficiaryId?: string }>();
const lovedOneName = params.lovedOneName || '';
const beneficiaryId = params.beneficiaryId;
const [isProcessing, setIsProcessing] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [step, setStep] = useState<'purchase' | 'order_placed'>('purchase');
const { user } = useAuth();
// const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet(); // Removed for web
// Check if equipment is already ordered - redirect to equipment-status
const checkEquipmentStatus = useCallback(async () => {
if (!beneficiaryId) {
setIsLoading(false);
return;
}
try {
const response = await api.getWellNuoBeneficiary(parseInt(beneficiaryId, 10));
if (response.ok && response.data) {
// If user already has devices - go to main screen
if (hasBeneficiaryDevices(response.data)) {
router.replace(`/(tabs)/beneficiaries/${beneficiaryId}`);
return;
}
// If equipment is ordered/shipped/delivered - go to equipment-status
const status = response.data.equipmentStatus;
if (status && ['ordered', 'shipped', 'delivered'].includes(status)) {
router.replace(`/(tabs)/beneficiaries/${beneficiaryId}/equipment-status`);
return;
}
}
} catch (error) {
console.warn('[Purchase] Failed to check equipment status:', error);
}
setIsLoading(false);
}, [beneficiaryId]);
useEffect(() => {
checkEquipmentStatus();
}, [checkEquipmentStatus]);
const handlePurchase = async () => {
alert('Purchases are currently only available in the mobile app.');
};
const handleAlreadyHaveSensors = () => {
router.replace({
pathname: '/(auth)/activate',
params: { beneficiaryId, lovedOneName },
});
};
const handleGoToEquipmentStatus = () => {
if (beneficiaryId) {
router.replace(`/(tabs)/beneficiaries/${beneficiaryId}/equipment-status`);
} else {
router.replace('/(tabs)');
}
};
// Loading state - checking equipment status
if (isLoading) {
return <LoadingSpinner fullScreen message="Loading..." />;
}
// Order Placed Screen
if (step === 'order_placed') {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.orderPlacedContainer}>
<View style={styles.successIcon}>
<Ionicons name="checkmark" size={64} color={AppColors.white} />
</View>
<Text style={styles.orderPlacedTitle}>Order Placed!</Text>
<Text style={styles.orderPlacedSubtitle}>
Thank you for your purchase
</Text>
<View style={styles.orderInfoCard}>
<View style={styles.orderInfoRow}>
<Text style={styles.orderInfoLabel}>Item</Text>
<Text style={styles.orderInfoValue}>{STARTER_KIT.name}</Text>
</View>
<View style={styles.orderInfoRow}>
<Text style={styles.orderInfoLabel}>For</Text>
<Text style={styles.orderInfoValue}>{lovedOneName || 'Your loved one'}</Text>
</View>
<View style={[styles.orderInfoRow, { borderBottomWidth: 0 }]}>
<Text style={styles.orderInfoLabel}>Total</Text>
<Text style={[styles.orderInfoValue, styles.orderInfoPrice]}>{STARTER_KIT.price}</Text>
</View>
</View>
<TouchableOpacity style={styles.primaryButton} onPress={handleGoToEquipmentStatus}>
<Text style={styles.primaryButtonText}>Track My Order</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.content}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => router.canGoBack() ? router.back() : router.replace('/(tabs)')}
>
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
<Text style={styles.title}>Get Started</Text>
<View style={styles.placeholder} />
</View>
{/* Product Card */}
<View style={styles.productCard}>
<View style={styles.productIcon}>
<Ionicons name="hardware-chip" size={48} color={AppColors.primary} />
</View>
<Text style={styles.productName}>{STARTER_KIT.name}</Text>
<Text style={styles.productPrice}>{STARTER_KIT.price}</Text>
<Text style={styles.productDescription}>
5 smart sensors that easily plug into any outlet and set up through the app in minutes
</Text>
{/* Security Badge */}
<View style={styles.securityBadge}>
<Ionicons name="shield-checkmark" size={16} color={AppColors.success} />
<Text style={styles.securityText}>Secure payment powered by Stripe</Text>
</View>
</View>
{/* Bottom Actions */}
<View style={styles.bottomActions}>
<TouchableOpacity
style={[styles.purchaseButton, isProcessing && styles.buttonDisabled]}
onPress={handlePurchase}
disabled={isProcessing}
>
{isProcessing ? (
<ActivityIndicator color={AppColors.white} />
) : (
<>
<Ionicons name="card" size={20} color={AppColors.white} />
<Text style={styles.purchaseButtonText}>Buy Now</Text>
</>
)}
</TouchableOpacity>
<TouchableOpacity style={styles.skipButton} onPress={handleAlreadyHaveSensors}>
<Text style={styles.skipButtonText}>I already have sensors</Text>
</TouchableOpacity>
<View style={{ marginTop: 20, padding: 10, backgroundColor: '#f0f9ff', borderRadius: 8 }}>
<Text style={{ textAlign: 'center', color: '#0066cc', fontSize: 14 }}>
Web version: Please use mobile app for purchases.
</Text>
</View>
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
content: {
flex: 1,
padding: Spacing.lg,
justifyContent: 'space-between',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
backButton: {
padding: Spacing.sm,
marginLeft: -Spacing.sm,
},
title: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
},
placeholder: {
width: 40,
},
productCard: {
backgroundColor: AppColors.white,
borderRadius: BorderRadius.xl,
padding: Spacing.xl,
alignItems: 'center',
borderWidth: 2,
borderColor: AppColors.primary,
...Shadows.md,
},
productIcon: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: `${AppColors.primary}15`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: Spacing.lg,
},
productName: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
textAlign: 'center',
marginBottom: Spacing.xs,
},
productPrice: {
fontSize: FontSizes['3xl'],
fontWeight: FontWeights.bold,
color: AppColors.primary,
marginBottom: Spacing.lg,
},
productDescription: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
lineHeight: 22,
marginBottom: Spacing.lg,
},
securityBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.xs,
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.md,
backgroundColor: `${AppColors.success}10`,
borderRadius: BorderRadius.lg,
},
securityText: {
fontSize: FontSizes.sm,
color: AppColors.success,
},
bottomActions: {
gap: Spacing.md,
},
purchaseButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.sm,
backgroundColor: AppColors.primary,
paddingVertical: Spacing.lg,
borderRadius: BorderRadius.lg,
...Shadows.primary,
},
buttonDisabled: {
opacity: 0.7,
},
purchaseButtonText: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
skipButton: {
alignItems: 'center',
paddingVertical: Spacing.sm,
},
skipButtonText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textDecorationLine: 'underline',
},
// Order Placed Screen
orderPlacedContainer: {
flex: 1,
padding: Spacing.lg,
alignItems: 'center',
justifyContent: 'center',
},
successIcon: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: AppColors.success,
alignItems: 'center',
justifyContent: 'center',
marginBottom: Spacing.xl,
...Shadows.lg,
},
orderPlacedTitle: {
fontSize: FontSizes['2xl'],
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
orderPlacedSubtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
marginBottom: Spacing.xl,
},
orderInfoCard: {
width: '100%',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.lg,
marginBottom: Spacing.xl,
...Shadows.sm,
},
orderInfoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: Spacing.sm,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
orderInfoLabel: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
orderInfoValue: {
fontSize: FontSizes.base,
fontWeight: FontWeights.medium,
color: AppColors.textPrimary,
},
orderInfoPrice: {
color: AppColors.primary,
fontWeight: FontWeights.bold,
},
primaryButton: {
width: '100%',
backgroundColor: AppColors.primary,
paddingVertical: Spacing.lg,
borderRadius: BorderRadius.lg,
alignItems: 'center',
...Shadows.primary,
},
primaryButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
});