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 { useAuth } from '@/contexts/AuthContext'; import { api } from '@/services/api'; import { hasBeneficiaryDevices } from '@/services/BeneficiaryDetailController'; import type { Beneficiary } 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 ( {text} ); } export default function SubscriptionScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const [isProcessing, setIsProcessing] = useState(false); const [isCanceling, setIsCanceling] = useState(false); const [beneficiary, setBeneficiary] = useState(null); const [isLoading, setIsLoading] = useState(true); const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet(); const { user } = useAuth(); useEffect(() => { loadBeneficiary(); }, [id]); const loadBeneficiary = async () => { if (!id) return; try { const response = await api.getWellNuoBeneficiary(parseInt(id, 10)); if (response.ok && response.data) { setBeneficiary(response.data); } else { console.error('Failed to load beneficiary:', response.error); } } catch (error) { console.error('Failed to load beneficiary:', error); } finally { setIsLoading(false); } }; const subscription = beneficiary?.subscription; const isActive = (subscription?.status === 'active' || subscription?.status === 'trialing') && subscription?.endDate && new Date(subscription.endDate) > new Date(); // Check if subscription is canceled but still active until period end const isCanceledButActive = isActive && subscription?.cancelAtPeriodEnd === true; const daysRemaining = subscription?.endDate ? Math.max(0, Math.ceil((new Date(subscription.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24))) : 0; // Self-guard: redirect if user shouldn't be on this page useEffect(() => { if (isLoading || !beneficiary || !id) return; // If no devices - redirect to purchase (waterfall priority) if (!hasBeneficiaryDevices(beneficiary)) { const status = beneficiary.equipmentStatus; if (status && ['ordered', 'shipped', 'delivered'].includes(status)) { router.replace(`/(tabs)/beneficiaries/${id}/equipment-status`); } else { router.replace(`/(tabs)/beneficiaries/${id}/purchase`); } return; } // If already has active subscription - redirect to dashboard if (isActive) { router.replace(`/(tabs)/beneficiaries/${id}`); } }, [beneficiary, isLoading, id, isActive]); const handleSubscribe = async () => { if (!beneficiary) { Alert.alert('Error', 'Beneficiary data not loaded.'); return; } setIsProcessing(true); try { // Get auth token const token = await api.getToken(); if (!token) { Alert.alert('Error', 'Please log in again'); setIsProcessing(false); return; } // 1. Create subscription payment sheet via Stripe Subscriptions API const response = await fetch(`${STRIPE_API_URL}/create-subscription-payment-sheet`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ beneficiaryId: beneficiary.id, }), }); const data = await response.json(); // Check if already subscribed if (data.alreadySubscribed) { Alert.alert( 'Already Subscribed!', `${beneficiary.name} already has an active subscription.` ); await loadBeneficiary(); return; } if (!data.clientSecret) { throw new Error(data.error || 'Failed to create subscription'); } // 2. Initialize the Payment Sheet const { error: initError } = await initPaymentSheet({ merchantDisplayName: 'WellNuo', paymentIntentClientSecret: data.clientSecret, customerId: data.customer, customerEphemeralKeySecret: data.ephemeralKey, 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! Fetch subscription status from Stripe const statusResponse = await fetch( `${STRIPE_API_URL}/subscription-status/${beneficiary.id}`, { headers: { 'Authorization': `Bearer ${token}`, }, } ); const statusData = await statusResponse.json(); // Reload beneficiary to get updated subscription await loadBeneficiary(); Alert.alert( 'Subscription Activated!', `Subscription for ${beneficiary.name} is now active.`, [{ 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 handleCancelSubscription = () => { if (!beneficiary) return; Alert.alert( 'Cancel Subscription?', `Are you sure you want to cancel the subscription for ${beneficiary.name}?\n\n• You'll keep access until ${subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'the end of the billing period'}\n• No refunds for remaining time\n• You can reactivate anytime before the period ends`, [ { text: 'Keep Subscription', style: 'cancel' }, { text: 'Cancel Subscription', style: 'destructive', onPress: confirmCancelSubscription, }, ] ); }; const confirmCancelSubscription = async () => { if (!beneficiary) return; setIsCanceling(true); try { const response = await api.cancelSubscription(beneficiary.id); if (!response.ok) { throw new Error(response.error || 'Failed to cancel subscription'); } // Reload beneficiary to get updated subscription status await loadBeneficiary(); Alert.alert( 'Subscription Canceled', `Your subscription will remain active until ${response.data?.cancelAt ? formatDate(new Date(response.data.cancelAt)) : 'the end of the billing period'}. You can reactivate anytime before then.`, [{ text: 'OK' }] ); } catch (error) { console.error('Cancel error:', error); Alert.alert( 'Cancellation Failed', error instanceof Error ? error.message : 'Something went wrong. Please try again.' ); } finally { setIsCanceling(false); } }; const handleReactivateSubscription = async () => { if (!beneficiary) return; setIsProcessing(true); try { // Get auth token const token = await api.getToken(); if (!token) { Alert.alert('Error', 'Please log in again'); setIsProcessing(false); return; } const response = await fetch(`${STRIPE_API_URL}/reactivate-subscription`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ beneficiaryId: beneficiary.id, }), }); const data = await response.json(); if (!response.ok || data.error) { throw new Error(data.error || 'Failed to reactivate subscription'); } // Reload beneficiary to get updated subscription status await loadBeneficiary(); Alert.alert( 'Subscription Reactivated!', 'Your subscription has been reactivated and will continue to renew automatically.', [{ text: 'Great!' }] ); } catch (error) { console.error('Reactivate error:', error); Alert.alert( 'Reactivation 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 ( router.back()}> Subscription ); } if (!beneficiary) { return ( router.back()}> Subscription Beneficiary Not Found Unable to load beneficiary information. ); } return ( {/* Header */} router.back()}> Subscription {/* Beneficiary Info */} {beneficiary.name.charAt(0).toUpperCase()} Subscription for {beneficiary.name} {/* Current Status */} {isActive ? ( isCanceledButActive ? ( // Subscription canceled but still active until period end <> ENDING SOON Active until {subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'N/A'} {daysRemaining} days left ) : ( // Active subscription <> ACTIVE Subscription is active Valid until {subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'N/A'} {daysRemaining} days remaining ) ) : ( <> {subscription?.status === 'expired' ? 'EXPIRED' : 'NO SUBSCRIPTION'} {subscription?.status === 'expired' ? 'Subscription has expired' : `Subscribe for ${beneficiary.name}`} Get full access to monitoring features )} {/* Subscription Card */} WellNuo ${SUBSCRIPTION_PRICE} /month {/* Show Subscribe button when not active */} {!isActive && ( {isProcessing ? ( ) : ( <> Subscribe — ${SUBSCRIPTION_PRICE}/month )} )} {/* Show Reactivate button when subscription is canceled but still active */} {isCanceledButActive && ( {isProcessing ? ( ) : ( <> Reactivate Subscription )} )} {/* Secure Payment Badge */} Secure payment powered by Stripe {/* Links section */} Restore Purchases {isActive && !isCanceledButActive && ( <> {isCanceling ? ( ) : ( Cancel )} )} {/* Terms */} Payment will be charged to your account at the confirmation of purchase. Subscription can be cancelled at any time from your account settings. ); } 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, }, cancelingBadge: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FEF3C7', paddingHorizontal: Spacing.md, paddingVertical: Spacing.xs, borderRadius: BorderRadius.full, gap: Spacing.xs, }, cancelingBadgeText: { fontSize: FontSizes.sm, fontWeight: FontWeights.bold, color: AppColors.warning, }, 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, }, reactivateButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: Spacing.sm, backgroundColor: AppColors.success, marginHorizontal: Spacing.lg, marginBottom: Spacing.lg, paddingVertical: Spacing.lg, borderRadius: BorderRadius.lg, }, securityBadge: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: Spacing.xs, marginTop: Spacing.lg, paddingVertical: Spacing.md, }, securityText: { fontSize: FontSizes.sm, color: AppColors.success, }, linksSection: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: Spacing.md, gap: Spacing.sm, }, linkButton: { paddingVertical: Spacing.xs, paddingHorizontal: Spacing.xs, }, linkButtonText: { fontSize: FontSizes.sm, color: AppColors.primary, }, linkButtonTextMuted: { fontSize: FontSizes.sm, color: AppColors.textMuted, }, linkDivider: { fontSize: FontSizes.sm, color: AppColors.textMuted, }, termsText: { fontSize: FontSizes.xs, color: AppColors.textMuted, textAlign: 'center', paddingHorizontal: Spacing.xl, paddingBottom: Spacing.xl, lineHeight: 16, }, });