WellNuo/app/(auth)/purchase.tsx

545 lines
16 KiB
TypeScript

import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
ActivityIndicator,
Alert,
} 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';
import { AppColors, Spacing, BorderRadius, FontSizes, FontWeights, Shadows } from '@/constants/theme';
import { useAuth } from '@/contexts/AuthContext';
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
import { api } from '@/services/api';
const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe';
const STARTER_KIT = {
name: 'WellNuo Starter Kit',
price: '$249',
priceValue: 249,
description: 'Everything you need to start monitoring your loved ones',
features: [
'Motion sensor (PIR)',
'Door/window sensor',
'Temperature & humidity sensor',
'WellNuo Hub',
'Mobile app access',
'1 year subscription included',
],
};
export default function PurchaseScreen() {
// Get lovedOneName from add-loved-one flow
const params = useLocalSearchParams<{ lovedOneName?: string; beneficiaryId?: string }>();
const lovedOneName = params.lovedOneName || '';
const beneficiaryId = params.beneficiaryId;
const [isProcessing, setIsProcessing] = useState(false);
const [step, setStep] = useState<'purchase' | 'order_placed'>('purchase');
const { user } = useAuth();
const { addLocalBeneficiary, updateLocalBeneficiary } = useBeneficiary();
const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet();
const handlePurchase = async () => {
setIsProcessing(true);
try {
// 1. Create Payment Sheet on our server
const response = await fetch(`${STRIPE_API_URL}/create-payment-sheet`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: user?.email || 'guest@wellnuo.com',
amount: STARTER_KIT.priceValue * 100, // Convert to cents ($249.00)
metadata: {
userId: user?.user_id || 'guest',
beneficiaryName: lovedOneName || 'To be configured',
},
}),
});
const data = await response.json();
if (!data.paymentIntent) {
throw new Error(data.error || 'Failed to create payment sheet');
}
// 2. Initialize the Payment Sheet
const { error: initError } = await initPaymentSheet({
merchantDisplayName: 'WellNuo',
paymentIntentClientSecret: data.paymentIntent,
customerId: data.customer,
customerEphemeralKeySecret: data.ephemeralKey,
defaultBillingDetails: {
email: user?.email || '',
},
returnURL: 'wellnuo://stripe-redirect',
applePay: {
merchantCountryCode: 'US',
},
googlePay: {
merchantCountryCode: 'US',
testEnv: true,
},
});
if (initError) {
throw new Error(initError.message);
}
// 3. Present the Payment Sheet
const { error: presentError } = await presentPaymentSheet();
if (presentError) {
if (presentError.code === 'Canceled') {
// User cancelled - do nothing
setIsProcessing(false);
return;
}
throw new Error(presentError.message);
}
// 4. Payment successful! Create or update beneficiary with 'ordered' status
if (beneficiaryId) {
// Update existing beneficiary
await updateLocalBeneficiary(parseInt(beneficiaryId, 10), {
equipmentStatus: 'ordered',
});
} else if (lovedOneName) {
// Create new beneficiary with ordered status
await addLocalBeneficiary({
name: lovedOneName,
equipmentStatus: 'ordered',
});
}
// Mark onboarding as completed
await api.setOnboardingCompleted(true);
// Show Order Placed screen
setStep('order_placed');
} catch (error) {
console.error('Payment error:', error);
Alert.alert(
'Payment Failed',
error instanceof Error ? error.message : 'Something went wrong. Please try again.'
);
}
setIsProcessing(false);
};
const handleSkip = () => {
router.replace({
pathname: '/(auth)/activate',
params: { lovedOneName },
});
};
const handleGoToHome = () => {
router.replace('/(tabs)');
};
// Order Placed Screen
if (step === 'order_placed') {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.orderPlacedContainer}>
{/* Success Icon */}
<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>
{/* Order Info */}
<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}>
<Text style={styles.orderInfoLabel}>Total</Text>
<Text style={[styles.orderInfoValue, styles.orderInfoPrice]}>{STARTER_KIT.price}</Text>
</View>
</View>
{/* What's Next */}
<View style={styles.whatsNextCard}>
<Text style={styles.whatsNextTitle}>What's Next?</Text>
<View style={styles.stepItem}>
<View style={styles.stepNumber}>
<Text style={styles.stepNumberText}>1</Text>
</View>
<View style={styles.stepContent}>
<Text style={styles.stepTitle}>Order Processing</Text>
<Text style={styles.stepDescription}>We'll prepare your kit for shipping</Text>
</View>
</View>
<View style={styles.stepItem}>
<View style={styles.stepNumber}>
<Text style={styles.stepNumberText}>2</Text>
</View>
<View style={styles.stepContent}>
<Text style={styles.stepTitle}>Shipping Notification</Text>
<Text style={styles.stepDescription}>You'll receive an email with tracking info</Text>
</View>
</View>
<View style={styles.stepItem}>
<View style={[styles.stepNumber, styles.stepNumberLast]}>
<Text style={styles.stepNumberText}>3</Text>
</View>
<View style={styles.stepContent}>
<Text style={styles.stepTitle}>Activate Your Kit</Text>
<Text style={styles.stepDescription}>Enter the serial number when delivered</Text>
</View>
</View>
</View>
{/* Actions */}
<View style={styles.orderPlacedActions}>
<TouchableOpacity style={styles.primaryButton} onPress={handleGoToHome}>
<Text style={styles.primaryButtonText}>Go to My Loved Ones</Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<ScrollView contentContainerStyle={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}>{STARTER_KIT.description}</Text>
{/* Features */}
<View style={styles.features}>
{STARTER_KIT.features.map((feature, index) => (
<View key={index} style={styles.featureRow}>
<Ionicons name="checkmark-circle" size={20} color={AppColors.success} />
<Text style={styles.featureText}>{feature}</Text>
</View>
))}
</View>
</View>
{/* Security Badge */}
<View style={styles.securityBadge}>
<Ionicons name="shield-checkmark" size={20} color={AppColors.success} />
<Text style={styles.securityText}>
Secure payment powered by Stripe
</Text>
</View>
</ScrollView>
{/* 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 - {STARTER_KIT.price}</Text>
</>
)}
</TouchableOpacity>
<TouchableOpacity style={styles.skipButton} onPress={handleSkip}>
<Text style={styles.skipButtonText}>I already have a kit</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
content: {
padding: Spacing.lg,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: Spacing.xl,
},
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,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
},
productIcon: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: `${AppColors.primary}15`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: Spacing.lg,
},
productName: {
fontSize: FontSizes['2xl'],
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
textAlign: 'center',
marginBottom: Spacing.sm,
},
productPrice: {
fontSize: FontSizes['3xl'],
fontWeight: FontWeights.bold,
color: AppColors.primary,
marginBottom: Spacing.sm,
},
productDescription: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
marginBottom: Spacing.xl,
},
features: {
width: '100%',
gap: Spacing.md,
},
featureRow: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.sm,
},
featureText: {
fontSize: FontSizes.base,
color: AppColors.textPrimary,
flex: 1,
},
securityBadge: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.sm,
marginTop: Spacing.xl,
paddingVertical: Spacing.md,
backgroundColor: `${AppColors.success}10`,
borderRadius: BorderRadius.lg,
},
securityText: {
fontSize: FontSizes.sm,
color: AppColors.success,
},
bottomActions: {
padding: Spacing.lg,
paddingBottom: Spacing.xl,
gap: Spacing.md,
},
purchaseButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.sm,
backgroundColor: AppColors.primary,
paddingVertical: Spacing.lg,
borderRadius: BorderRadius.lg,
},
buttonDisabled: {
opacity: 0.7,
},
purchaseButtonText: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
skipButton: {
alignItems: 'center',
paddingVertical: Spacing.md,
},
skipButtonText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textDecorationLine: 'underline',
},
// Order Placed Screen Styles
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.lg,
...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,
},
whatsNextCard: {
width: '100%',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.lg,
marginBottom: Spacing.xl,
...Shadows.sm,
},
whatsNextTitle: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
marginBottom: Spacing.lg,
},
stepItem: {
flexDirection: 'row',
marginBottom: Spacing.md,
},
stepNumber: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: AppColors.primary,
alignItems: 'center',
justifyContent: 'center',
marginRight: Spacing.md,
},
stepNumberLast: {
backgroundColor: AppColors.textMuted,
},
stepNumberText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.bold,
color: AppColors.white,
},
stepContent: {
flex: 1,
},
stepTitle: {
fontSize: FontSizes.base,
fontWeight: FontWeights.medium,
color: AppColors.textPrimary,
},
stepDescription: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginTop: 2,
},
orderPlacedActions: {
width: '100%',
},
primaryButton: {
backgroundColor: AppColors.primary,
paddingVertical: Spacing.lg,
borderRadius: BorderRadius.lg,
alignItems: 'center',
...Shadows.primary,
},
primaryButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
});