import React, { useState, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Alert, ActivityIndicator, Modal, ScrollView, Linking, } 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 { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu'; import type { Beneficiary } from '@/types'; const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe'; const SUBSCRIPTION_PRICE = 49; type SubscriptionState = 'active' | 'canceling' | 'none'; 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 [showSuccessModal, setShowSuccessModal] = useState(false); const [justSubscribed, setJustSubscribed] = useState(false); const [transactions, setTransactions] = useState>([]); const [isLoadingTransactions, setIsLoadingTransactions] = useState(false); const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet(); const { user } = useAuth(); useEffect(() => { loadBeneficiary(); loadTransactions(); }, [id]); const loadTransactions = async () => { if (!id) return; setIsLoadingTransactions(true); try { const response = await api.getTransactionHistory(parseInt(id, 10)); if (response.ok && response.data) { setTransactions(response.data.transactions); } } catch (error) { // Silently ignore } finally { setIsLoadingTransactions(false); } }; const loadBeneficiary = async () => { if (!id) return; try { const response = await api.getWellNuoBeneficiary(parseInt(id, 10)); if (response.ok && response.data) { setBeneficiary(response.data); } } catch (error) { // Silently ignore } finally { setIsLoading(false); } }; const subscription = beneficiary?.subscription; // Determine subscription state const getSubscriptionState = (): SubscriptionState => { if (!subscription) return 'none'; const isStatusActive = subscription.status === 'active' || subscription.status === 'trialing'; const isNotExpired = !subscription.endDate || new Date(subscription.endDate) > new Date(); if (isStatusActive && isNotExpired) { return subscription.cancelAtPeriodEnd ? 'canceling' : 'active'; } return 'none'; }; const subscriptionState = getSubscriptionState(); const handleSubscribe = async () => { if (!beneficiary) { Alert.alert('Error', 'Beneficiary data not loaded.'); return; } setIsProcessing(true); try { const token = await api.getToken(); if (!token) { Alert.alert('Error', 'Please log in again'); setIsProcessing(false); return; } 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 }), }); if (!response.ok) { const errorText = await response.text(); let errorMessage = 'Failed to create payment'; try { const errorJson = JSON.parse(errorText); errorMessage = errorJson.error || errorJson.message || errorMessage; } catch { errorMessage = `Server error (${response.status})`; } throw new Error(errorMessage); } const data = await response.json(); if (data.alreadySubscribed) { Alert.alert('Already Subscribed!', `${beneficiary.displayName} already has an active subscription.`); await loadBeneficiary(); return; } if (!data.clientSecret) { throw new Error(data.error || 'Failed to create subscription - no clientSecret'); } const isSetupIntent = data.clientSecret.startsWith('seti_'); // Build payment sheet params based on intent type const baseParams = { merchantDisplayName: 'WellNuo', customerId: data.customer, ...(data.customerSessionClientSecret ? { customerSessionClientSecret: data.customerSessionClientSecret } : { customerEphemeralKeySecret: data.ephemeralKey }), returnURL: 'wellnuo://stripe-redirect', applePay: { merchantCountryCode: 'US' }, googlePay: { merchantCountryCode: 'US', testEnv: true }, }; const paymentSheetParams = isSetupIntent ? { ...baseParams, setupIntentClientSecret: data.clientSecret } : { ...baseParams, paymentIntentClientSecret: data.clientSecret }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const { error: initError } = await initPaymentSheet(paymentSheetParams as any); if (initError) { throw new Error(initError.message); } const { error: presentError } = await presentPaymentSheet(); if (presentError) { if (presentError.code === 'Canceled') { setIsProcessing(false); return; } throw new Error(presentError.message); } // For PaymentIntent (subscription with immediate payment), the subscription // is automatically activated via Stripe webhook when payment succeeds. // No need to call confirm endpoint for PaymentIntent. if (isSetupIntent && data.subscriptionId) { // Only for SetupIntent flow (future payment), we need to confirm const confirmResponse = await fetch(`${STRIPE_API_URL}/confirm-subscription-payment`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ subscriptionId: data.subscriptionId, beneficiaryId: beneficiary.id, }), }); const confirmData = await confirmResponse.json(); if (!confirmResponse.ok || confirmData.error) { throw new Error(confirmData.error || 'Failed to confirm subscription'); } } // Wait a moment for webhook to process await new Promise(resolve => setTimeout(resolve, 2000)); setJustSubscribed(true); await loadBeneficiary(); setShowSuccessModal(true); } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Something went wrong'; Alert.alert('Payment Failed', errorMsg); } finally { setIsProcessing(false); } }; const handleCancelSubscription = () => { if (!beneficiary) return; Alert.alert( 'Cancel Subscription?', `Your subscription will remain active until ${subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'the end of your billing period'}. You can reactivate anytime before then.`, [ { text: 'Keep Subscription', style: 'cancel' }, { text: 'Cancel', 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?.message || 'Failed to cancel subscription'); await loadBeneficiary(); Alert.alert('Subscription Canceled', 'You can reactivate anytime before the period ends.'); } catch (error) { Alert.alert('Error', error instanceof Error ? error.message : 'Something went wrong.'); } finally { setIsCanceling(false); } }; const handleReactivateSubscription = async () => { if (!beneficiary) return; setIsProcessing(true); try { 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'); } await loadBeneficiary(); Alert.alert('Reactivated!', 'Your subscription will continue to renew automatically.'); } catch (error) { Alert.alert('Error', error instanceof Error ? error.message : 'Something went wrong.'); } finally { setIsProcessing(false); } }; const formatDate = (date: Date) => { return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }; const openReceipt = (url?: string) => { if (url) Linking.openURL(url); }; const handleSuccessModalClose = () => { setShowSuccessModal(false); router.replace(`/(tabs)/beneficiaries/${id}`); }; // Loading state if (isLoading) { return ( router.back()}> Subscription ); } if (!beneficiary) { return ( router.back()}> Subscription Unable to load beneficiary ); } // Render subscription status card based on state const renderStatusCard = () => { switch (subscriptionState) { case 'active': return ( Active Subscription {subscription?.endDate ? `Renews ${formatDate(new Date(subscription.endDate))}` : 'Renews monthly'} ${SUBSCRIPTION_PRICE} /month ); case 'canceling': return ( Subscription Ending Access until {subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'period end'} After this date, monitoring and alerts for {beneficiary.displayName} will stop. ); case 'none': default: return ( No Active Subscription Subscribe to unlock monitoring for {beneficiary.displayName} ${SUBSCRIPTION_PRICE} /month ); } }; // Render action button based on state const renderActionButton = () => { switch (subscriptionState) { case 'active': return ( {isCanceling ? ( ) : ( Cancel Subscription )} ); case 'canceling': return ( {isProcessing ? ( ) : ( <> Reactivate Subscription )} ); case 'none': default: return ( {isProcessing ? ( ) : ( <> Subscribe )} ); } }; return ( {/* Header */} router.back()}> Subscription {/* Status Card */} {renderStatusCard()} {/* Action Button */} {renderActionButton()} {/* Security note */} Secure payment by Stripe {/* Transaction History */} {transactions.length > 0 && ( Payment History {transactions.map((tx) => ( openReceipt(tx.invoicePdf || tx.hostedUrl || tx.receiptUrl)} disabled={!tx.invoicePdf && !tx.hostedUrl && !tx.receiptUrl} > {tx.description} {formatDate(new Date(tx.date))} ${tx.amount.toFixed(2)} {(tx.invoicePdf || tx.hostedUrl || tx.receiptUrl) && ( )} ))} )} {/* Success Modal */} Subscription Active! Monitoring for {beneficiary?.name} is now enabled. Continue ); } 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, }, centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, errorText: { fontSize: FontSizes.base, color: AppColors.textMuted, }, scrollContent: { flex: 1, }, content: { padding: Spacing.lg, paddingBottom: Spacing.xxl, }, contentCentered: { flexGrow: 1, justifyContent: 'center', }, // Status Card Styles statusCard: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, padding: Spacing.xl, alignItems: 'center', borderWidth: 2, borderColor: AppColors.success, ...Shadows.sm, }, statusCardCanceling: { borderColor: '#F59E0B', backgroundColor: '#FFFBEB', }, statusCardNone: { borderColor: AppColors.border, backgroundColor: AppColors.surface, }, statusIconActive: { width: 56, height: 56, borderRadius: 28, backgroundColor: AppColors.success, alignItems: 'center', justifyContent: 'center', marginBottom: Spacing.md, }, statusIconCanceling: { width: 56, height: 56, borderRadius: 28, backgroundColor: '#F59E0B', alignItems: 'center', justifyContent: 'center', marginBottom: Spacing.md, }, statusIconNone: { width: 56, height: 56, borderRadius: 28, backgroundColor: AppColors.surfaceSecondary, alignItems: 'center', justifyContent: 'center', marginBottom: Spacing.md, }, statusTitle: { fontSize: FontSizes.xl, fontWeight: FontWeights.bold, color: AppColors.textPrimary, marginBottom: Spacing.xs, }, statusTitleNone: { fontSize: FontSizes.xl, fontWeight: FontWeights.bold, color: AppColors.textSecondary, marginBottom: Spacing.xs, }, statusSubtitle: { fontSize: FontSizes.sm, color: AppColors.success, marginBottom: Spacing.md, }, statusSubtitleCanceling: { fontSize: FontSizes.sm, color: '#B45309', fontWeight: FontWeights.medium, marginBottom: Spacing.sm, }, statusSubtitleNone: { fontSize: FontSizes.sm, color: AppColors.textMuted, textAlign: 'center', marginBottom: Spacing.md, }, cancelingNote: { fontSize: FontSizes.sm, color: '#92400E', textAlign: 'center', lineHeight: 20, }, priceRow: { flexDirection: 'row', alignItems: 'baseline', }, priceAmount: { fontSize: FontSizes['2xl'], fontWeight: FontWeights.bold, color: AppColors.primary, }, pricePeriod: { fontSize: FontSizes.base, color: AppColors.textMuted, marginLeft: 2, }, // Action Section actionSection: { marginTop: Spacing.xl, gap: Spacing.md, }, primaryButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: Spacing.sm, backgroundColor: AppColors.primary, paddingVertical: Spacing.md, borderRadius: BorderRadius.lg, ...Shadows.sm, }, reactivateButton: { backgroundColor: AppColors.success, }, buttonDisabled: { opacity: 0.7, }, primaryButtonText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.white, }, cancelButton: { paddingVertical: Spacing.sm, alignItems: 'center', }, cancelButtonText: { fontSize: FontSizes.sm, color: AppColors.textMuted, textDecorationLine: 'underline', }, securityRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: Spacing.xs, }, securityText: { fontSize: FontSizes.xs, color: AppColors.textMuted, }, // Transaction Section transactionSection: { marginTop: Spacing.xl, }, sectionTitle: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, marginBottom: Spacing.md, }, transactionItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: AppColors.surface, padding: Spacing.md, borderRadius: BorderRadius.md, marginBottom: Spacing.sm, }, transactionLeft: { flex: 1, }, transactionDesc: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.textPrimary, }, transactionDate: { fontSize: FontSizes.xs, color: AppColors.textMuted, marginTop: 2, }, transactionRight: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, }, transactionAmount: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, }, // Modal modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center', padding: Spacing.lg, }, modalContent: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, padding: Spacing.xl, width: '100%', maxWidth: 320, alignItems: 'center', ...Shadows.lg, }, modalIcon: { marginBottom: Spacing.md, }, modalTitle: { fontSize: FontSizes.xl, fontWeight: FontWeights.bold, color: AppColors.textPrimary, marginBottom: Spacing.sm, }, modalMessage: { fontSize: FontSizes.base, color: AppColors.textSecondary, textAlign: 'center', marginBottom: Spacing.lg, }, modalButton: { backgroundColor: AppColors.primary, paddingVertical: Spacing.md, paddingHorizontal: Spacing.xl, borderRadius: BorderRadius.lg, width: '100%', }, modalButtonText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.white, textAlign: 'center', }, });