WellNuo/app/(tabs)/beneficiaries/[id]/subscription.tsx

629 lines
18 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
} 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';
import { AppColors, BorderRadius, FontSizes, FontWeights, Spacing, Shadows } from '@/constants/theme';
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
import { useAuth } from '@/contexts/AuthContext';
import type { Beneficiary, BeneficiarySubscription } from '@/types';
const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe';
const SUBSCRIPTION_PRICE = 49; // $49/month
interface PlanFeatureProps {
text: string;
included: boolean;
}
function PlanFeature({ text, included }: PlanFeatureProps) {
return (
<View style={styles.featureRow}>
<Ionicons
name={included ? 'checkmark-circle' : 'close-circle'}
size={20}
color={included ? AppColors.success : AppColors.textMuted}
/>
<Text style={[styles.featureText, !included && styles.featureTextDisabled]}>
{text}
</Text>
</View>
);
}
export default function SubscriptionScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const [isProcessing, setIsProcessing] = useState(false);
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
const [isLoading, setIsLoading] = useState(true);
const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet();
const { user } = useAuth();
const { getBeneficiaryById, updateLocalBeneficiary } = useBeneficiary();
useEffect(() => {
loadBeneficiary();
}, [id]);
const loadBeneficiary = async () => {
if (!id) return;
try {
const data = await getBeneficiaryById(id);
setBeneficiary(data);
} catch (error) {
console.error('Failed to load beneficiary:', error);
} finally {
setIsLoading(false);
}
};
const subscription = beneficiary?.subscription;
const isActive = subscription?.status === 'active' &&
subscription?.endDate &&
new Date(subscription.endDate) > new Date();
const daysRemaining = subscription?.endDate
? Math.max(0, Math.ceil((new Date(subscription.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)))
: 0;
const handleSubscribe = async () => {
if (!beneficiary) {
Alert.alert('Error', 'Beneficiary data not loaded.');
return;
}
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: SUBSCRIPTION_PRICE * 100, // Convert to cents ($49.00)
metadata: {
type: 'subscription',
planType: 'monthly',
userId: user?.user_id || 'guest',
beneficiaryId: beneficiary.id,
beneficiaryName: beneficiary.name,
},
}),
});
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') {
setIsProcessing(false);
return;
}
throw new Error(presentError.message);
}
// 4. Payment successful! Save subscription to beneficiary
const now = new Date();
const endDate = new Date(now);
endDate.setMonth(endDate.getMonth() + 1); // 1 month subscription
const newSubscription: BeneficiarySubscription = {
status: 'active',
startDate: now.toISOString(),
endDate: endDate.toISOString(),
planType: 'monthly',
price: SUBSCRIPTION_PRICE,
};
await updateLocalBeneficiary(beneficiary.id, {
subscription: newSubscription,
});
// Reload beneficiary to get updated subscription
await loadBeneficiary();
Alert.alert(
'Subscription Activated!',
`Subscription for ${beneficiary.name} is now active until ${formatDate(endDate)}.`,
[{ text: 'Great!' }]
);
} catch (error) {
console.error('Payment error:', error);
Alert.alert(
'Payment Failed',
error instanceof Error ? error.message : 'Something went wrong. Please try again.'
);
} finally {
setIsProcessing(false);
}
};
const handleRestorePurchases = () => {
Alert.alert(
'Restoring Purchases',
'Looking for previous purchases...',
[{ text: 'OK' }]
);
setTimeout(() => {
Alert.alert('No Purchases Found', 'We couldn\'t find any previous purchases associated with your account.');
}, 1500);
};
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
if (isLoading) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<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}>Subscription</Text>
<View style={styles.placeholder} />
</View>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} />
</View>
</SafeAreaView>
);
}
if (!beneficiary) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<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}>Subscription</Text>
<View style={styles.placeholder} />
</View>
<View style={styles.noBeneficiaryContainer}>
<View style={styles.noBeneficiaryIcon}>
<Ionicons name="person-outline" size={48} color={AppColors.textMuted} />
</View>
<Text style={styles.noBeneficiaryTitle}>Beneficiary Not Found</Text>
<Text style={styles.noBeneficiaryText}>
Unable to load beneficiary information.
</Text>
</View>
</SafeAreaView>
);
}
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}>Subscription</Text>
<View style={styles.placeholder} />
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Beneficiary Info */}
<View style={styles.beneficiaryBanner}>
<View style={styles.beneficiaryAvatar}>
<Text style={styles.beneficiaryAvatarText}>
{beneficiary.name.charAt(0).toUpperCase()}
</Text>
</View>
<View style={styles.beneficiaryInfo}>
<Text style={styles.beneficiaryLabel}>Subscription for</Text>
<Text style={styles.beneficiaryName}>{beneficiary.name}</Text>
</View>
</View>
{/* Current Status */}
<View style={styles.statusBanner}>
{isActive ? (
<>
<View style={styles.activeBadge}>
<Ionicons name="checkmark-circle" size={20} color={AppColors.success} />
<Text style={styles.activeBadgeText}>ACTIVE</Text>
</View>
<Text style={styles.statusTitle}>Subscription is active</Text>
<Text style={styles.statusDescription}>
Valid until {subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'N/A'}
</Text>
<View style={styles.daysRemaining}>
<Text style={styles.daysRemainingNumber}>{daysRemaining}</Text>
<Text style={styles.daysRemainingLabel}>days remaining</Text>
</View>
</>
) : (
<>
<View style={styles.inactiveBadge}>
<Ionicons name="close-circle" size={20} color={AppColors.error} />
<Text style={styles.inactiveBadgeText}>
{subscription?.status === 'expired' ? 'EXPIRED' : 'NO SUBSCRIPTION'}
</Text>
</View>
<Text style={styles.statusTitle}>
{subscription?.status === 'expired'
? 'Subscription has expired'
: `Subscribe for ${beneficiary.name}`}
</Text>
<Text style={styles.statusDescription}>
Get full access to monitoring features
</Text>
</>
)}
</View>
{/* Subscription Card */}
<View style={styles.section}>
<View style={styles.subscriptionCard}>
<View style={styles.cardHeader}>
<View style={styles.proBadge}>
<Ionicons name="shield-checkmark" size={20} color={AppColors.primary} />
<Text style={styles.proBadgeText}>WellNuo</Text>
</View>
<View style={styles.priceContainer}>
<Text style={styles.priceAmount}>${SUBSCRIPTION_PRICE}</Text>
<Text style={styles.pricePeriod}>/month</Text>
</View>
</View>
<View style={styles.featuresContainer}>
<PlanFeature text="Real-time activity monitoring" included={true} />
<PlanFeature text="AI-powered wellness insights" included={true} />
<PlanFeature text="Instant alert notifications" included={true} />
<PlanFeature text="Voice AI companion (Julia)" included={true} />
<PlanFeature text="Activity history & trends" included={true} />
<PlanFeature text="Family sharing" included={true} />
<PlanFeature text="24/7 Priority support" included={true} />
</View>
{!isActive && (
<TouchableOpacity
style={[styles.subscribeButton, isProcessing && styles.buttonDisabled]}
onPress={handleSubscribe}
disabled={isProcessing}
>
{isProcessing ? (
<ActivityIndicator color={AppColors.white} />
) : (
<>
<Ionicons name="card" size={20} color={AppColors.white} />
<Text style={styles.subscribeButtonText}>
Subscribe ${SUBSCRIPTION_PRICE}/month
</Text>
</>
)}
</TouchableOpacity>
)}
</View>
</View>
{/* Secure Payment Badge */}
<View style={styles.securityBadge}>
<Ionicons name="lock-closed" size={16} color={AppColors.success} />
<Text style={styles.securityText}>Secure payment powered by Stripe</Text>
</View>
{/* Restore Purchases */}
<TouchableOpacity style={styles.restoreButton} onPress={handleRestorePurchases}>
<Text style={styles.restoreButtonText}>Restore Purchases</Text>
</TouchableOpacity>
{/* Terms */}
<Text style={styles.termsText}>
Payment will be charged to your account at the confirmation of purchase.
Subscription can be cancelled at any time from your account settings.
</Text>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
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,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
// No beneficiary state
noBeneficiaryContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: Spacing.xl,
},
noBeneficiaryIcon: {
width: 96,
height: 96,
borderRadius: 48,
backgroundColor: AppColors.surfaceSecondary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.lg,
},
noBeneficiaryTitle: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
marginBottom: Spacing.sm,
},
noBeneficiaryText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
lineHeight: 24,
},
// Beneficiary banner
beneficiaryBanner: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.primaryLighter,
marginHorizontal: Spacing.lg,
marginTop: Spacing.md,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
},
beneficiaryAvatar: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
},
beneficiaryAvatarText: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.white,
},
beneficiaryInfo: {
marginLeft: Spacing.md,
},
beneficiaryLabel: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
beneficiaryName: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
},
// Status banner
statusBanner: {
backgroundColor: AppColors.background,
padding: Spacing.xl,
alignItems: 'center',
},
activeBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#D1FAE5',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full,
gap: Spacing.xs,
},
activeBadgeText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.bold,
color: AppColors.success,
},
inactiveBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FEE2E2',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full,
gap: Spacing.xs,
},
inactiveBadgeText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.bold,
color: AppColors.error,
},
statusTitle: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
marginTop: Spacing.md,
textAlign: 'center',
},
statusDescription: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
marginTop: Spacing.xs,
textAlign: 'center',
},
daysRemaining: {
marginTop: Spacing.lg,
alignItems: 'center',
},
daysRemainingNumber: {
fontSize: 48,
fontWeight: FontWeights.bold,
color: AppColors.primary,
},
daysRemainingLabel: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
section: {
marginTop: Spacing.md,
},
subscriptionCard: {
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
borderRadius: BorderRadius.lg,
overflow: 'hidden',
borderWidth: 2,
borderColor: AppColors.primary,
},
cardHeader: {
backgroundColor: `${AppColors.primary}10`,
padding: Spacing.lg,
alignItems: 'center',
},
proBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.xs,
},
proBadgeText: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.bold,
color: AppColors.primary,
},
priceContainer: {
flexDirection: 'row',
alignItems: 'baseline',
marginTop: Spacing.md,
},
priceAmount: {
fontSize: 48,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
},
pricePeriod: {
fontSize: FontSizes.lg,
color: AppColors.textSecondary,
marginLeft: Spacing.xs,
},
featuresContainer: {
padding: Spacing.lg,
},
featureRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.xs,
},
featureText: {
fontSize: FontSizes.sm,
color: AppColors.textPrimary,
marginLeft: Spacing.sm,
},
featureTextDisabled: {
color: AppColors.textMuted,
},
subscribeButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.sm,
backgroundColor: AppColors.primary,
marginHorizontal: Spacing.lg,
marginBottom: Spacing.lg,
paddingVertical: Spacing.lg,
borderRadius: BorderRadius.lg,
},
buttonDisabled: {
opacity: 0.7,
},
subscribeButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
securityBadge: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.xs,
marginTop: Spacing.lg,
paddingVertical: Spacing.md,
},
securityText: {
fontSize: FontSizes.sm,
color: AppColors.success,
},
restoreButton: {
alignItems: 'center',
paddingVertical: Spacing.md,
},
restoreButtonText: {
fontSize: FontSizes.sm,
color: AppColors.primary,
},
termsText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
textAlign: 'center',
paddingHorizontal: Spacing.xl,
paddingBottom: Spacing.xl,
lineHeight: 16,
},
});