import React, { useState, useEffect } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Alert, ActivityIndicator, Modal, ScrollView, Clipboard, } 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'; import * as ExpoClipboard from 'expo-clipboard'; import Toast from 'react-native-root-toast'; const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe'; const SUBSCRIPTION_PRICE = 49; // $49/month // DEBUG MODE - set to true to show debug panel const DEBUG_MODE = true; 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); // Prevent self-guard redirect after payment // Debug state const [debugLogs, setDebugLogs] = useState([]); const [showDebugPanel, setShowDebugPanel] = useState(DEBUG_MODE); const addDebugLog = (message: string) => { const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; setDebugLogs(prev => [...prev, `[${timestamp}] ${message}`]); console.log(`[DEBUG] ${message}`); }; const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet(); const { user } = useAuth(); useEffect(() => { loadBeneficiary(); }, [id]); const loadBeneficiary = async () => { if (!id) { addDebugLog(`loadBeneficiary: no id provided`); return; } addDebugLog(`loadBeneficiary: fetching id=${id}`); try { const response = await api.getWellNuoBeneficiary(parseInt(id, 10)); addDebugLog(`loadBeneficiary: response.ok=${response.ok}`); if (response.ok && response.data) { setBeneficiary(response.data); addDebugLog(`loadBeneficiary: got beneficiary id=${response.data.id}, name=${response.data.name}`); } else { addDebugLog(`loadBeneficiary: ERROR - ${response.error}`); console.error('Failed to load beneficiary:', response.error); } } catch (error) { addDebugLog(`loadBeneficiary: EXCEPTION - ${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; // Self-guard: redirect if user shouldn't be on this page useEffect(() => { if (isLoading || !beneficiary || !id) return; // Don't redirect if we just subscribed and modal is showing if (justSubscribed || showSuccessModal) 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, justSubscribed, showSuccessModal]); const handleSubscribe = async () => { addDebugLog(`handleSubscribe: START`); addDebugLog(`handleSubscribe: beneficiary=${JSON.stringify(beneficiary ? {id: beneficiary.id, name: beneficiary.name} : null)}`); if (!beneficiary) { addDebugLog(`handleSubscribe: ERROR - no beneficiary`); Alert.alert('Error', 'Beneficiary data not loaded.'); return; } setIsProcessing(true); try { // Get auth token const token = await api.getToken(); addDebugLog(`handleSubscribe: token=${token ? token.substring(0, 20) + '...' : 'null'}`); if (!token) { addDebugLog(`handleSubscribe: ERROR - no token`); Alert.alert('Error', 'Please log in again'); setIsProcessing(false); return; } // 1. Create subscription payment sheet via Stripe Subscriptions API const requestBody = { beneficiaryId: beneficiary.id }; addDebugLog(`handleSubscribe: calling ${STRIPE_API_URL}/create-subscription-payment-sheet`); addDebugLog(`handleSubscribe: body=${JSON.stringify(requestBody)}`); const response = await fetch(`${STRIPE_API_URL}/create-subscription-payment-sheet`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify(requestBody), }); addDebugLog(`handleSubscribe: response.status=${response.status}`); // Check if response is OK before parsing JSON if (!response.ok) { const errorText = await response.text(); addDebugLog(`handleSubscribe: ERROR response - ${errorText}`); 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(); addDebugLog(`handleSubscribe: response data=${JSON.stringify(data)}`); // 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 // Determine if clientSecret is for PaymentIntent (pi_) or SetupIntent (seti_) const isSetupIntent = data.clientSecret.startsWith('seti_'); const paymentSheetParams: Parameters[0] = { merchantDisplayName: 'WellNuo', customerId: data.customer, // Use Customer Session for showing saved payment methods (new API) // Falls back to Ephemeral Key for backwards compatibility ...(data.customerSessionClientSecret ? { customerSessionClientSecret: data.customerSessionClientSecret } : { customerEphemeralKeySecret: data.ephemeralKey }), returnURL: 'wellnuo://stripe-redirect', applePay: { merchantCountryCode: 'US', }, googlePay: { merchantCountryCode: 'US', testEnv: true, }, }; // Use correct parameter based on secret type if (isSetupIntent) { paymentSheetParams.setupIntentClientSecret = data.clientSecret; } else { paymentSheetParams.paymentIntentClientSecret = data.clientSecret; } const { error: initError } = await initPaymentSheet(paymentSheetParams); 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! Create subscription with the payment method from SetupIntent addDebugLog(`handleSubscribe: PaymentSheet completed, creating subscription...`); console.log('[Subscription] Payment Sheet completed, creating subscription...'); const confirmResponse = await fetch( `${STRIPE_API_URL}/confirm-subscription-payment`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ setupIntentId: data.setupIntentId, beneficiaryId: beneficiary.id, }), } ); const confirmData = await confirmResponse.json(); addDebugLog(`handleSubscribe: confirm response=${JSON.stringify(confirmData)}`); console.log('[Subscription] Confirm response:', confirmData); if (!confirmResponse.ok || confirmData.error) { throw new Error(confirmData.error || 'Failed to create subscription'); } // 5. 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(); // Mark as just subscribed to prevent self-guard redirect during modal setJustSubscribed(true); // Reload beneficiary to get updated subscription await loadBeneficiary(); // Show success modal instead of Alert setShowSuccessModal(true); } 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 formatDate = (date: Date) => { return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); }; const handleSuccessModalClose = () => { setShowSuccessModal(false); // Navigate to beneficiary dashboard after closing modal router.replace(`/(tabs)/beneficiaries/${id}`); }; if (isLoading) { return ( router.back()}> Subscription ); } if (!beneficiary) { return ( router.back()}> Subscription Beneficiary Not Found Unable to load beneficiary information. ); } const copyDebugLogs = async () => { const logsText = debugLogs.join('\n'); try { await ExpoClipboard.setStringAsync(logsText); Toast.show('Debug logs copied to clipboard!', { duration: Toast.durations.SHORT, position: Toast.positions.BOTTOM, }); } catch (e) { Alert.alert('Copied!', 'Debug logs copied to clipboard'); } }; return ( {/* Debug Panel */} {showDebugPanel && ( 🐛 Debug Panel Copy setDebugLogs([])} style={styles.debugClearBtn}> Clear setShowDebugPanel(false)} style={styles.debugCloseBtn}> {debugLogs.length === 0 ? ( No logs yet. Press Subscribe to see logs. ) : ( debugLogs.map((log, i) => ( {log} )) )} )} {/* Header */} router.back()}> Subscription setShowDebugPanel(!showDebugPanel)}> {/* Subscription Card */} WellNuo Subscription ${SUBSCRIPTION_PRICE} /month Full access to real-time monitoring, AI insights, alerts, voice companion, and family sharing for {beneficiary.name} {/* Status indicator */} {isActive && ( {isCanceledButActive ? `Ends ${subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'soon'}` : `Active until ${subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'N/A'}`} )} {/* Security Badge */} Secure payment by Stripe {/* Actions */} {/* Subscribe button when not active */} {!isActive && ( {isProcessing ? ( ) : ( <> Subscribe )} )} {/* Reactivate button when canceled but still active */} {isCanceledButActive && ( {isProcessing ? ( ) : ( <> Reactivate Subscription )} )} {/* Cancel link - only show when active subscription */} {isActive && !isCanceledButActive && ( {isCanceling ? ( ) : ( Cancel Subscription )} )} {/* Success Modal */} Subscription Activated! Subscription for {beneficiary?.name} is now active. 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, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, 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, }, content: { flex: 1, padding: Spacing.lg, justifyContent: 'center', alignItems: 'center', }, subscriptionCard: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, padding: Spacing.lg, borderWidth: 2, borderColor: AppColors.primary, width: '100%', ...Shadows.md, }, cardHeader: { alignItems: 'center', }, proBadgeText: { fontSize: FontSizes.lg, fontWeight: FontWeights.bold, color: AppColors.textPrimary, }, priceContainer: { flexDirection: 'row', alignItems: 'baseline', marginTop: Spacing.sm, }, priceAmount: { fontSize: FontSizes['3xl'], fontWeight: FontWeights.bold, color: AppColors.primary, }, pricePeriod: { fontSize: FontSizes.lg, color: AppColors.textSecondary, marginLeft: Spacing.xs, }, planDescription: { fontSize: FontSizes.base, color: AppColors.textSecondary, textAlign: 'center', lineHeight: 22, marginTop: Spacing.md, }, activeBadge: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#D1FAE5', paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, borderRadius: BorderRadius.lg, gap: Spacing.xs, marginTop: Spacing.md, }, activeBadgeText: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.success, }, cancelingBadge: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FEF3C7', paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, borderRadius: BorderRadius.lg, gap: Spacing.xs, marginTop: Spacing.md, }, cancelingBadgeText: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.warning, }, securityBadge: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: Spacing.md, gap: Spacing.xs, }, securityText: { fontSize: FontSizes.xs, color: AppColors.success, }, actionsSection: { width: '100%', gap: Spacing.md, marginTop: Spacing.xl, }, subscribeButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: Spacing.sm, backgroundColor: AppColors.primary, paddingVertical: Spacing.md, 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, paddingVertical: Spacing.md, borderRadius: BorderRadius.lg, }, linkButton: { paddingVertical: Spacing.xs, alignItems: 'center', }, linkButtonTextMuted: { fontSize: FontSizes.sm, color: AppColors.textMuted, textDecorationLine: 'underline', }, // Modal styles 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: 340, alignItems: 'center', ...Shadows.lg, }, modalIconContainer: { marginBottom: Spacing.md, }, modalTitle: { fontSize: FontSizes.xl, fontWeight: FontWeights.bold, color: AppColors.textPrimary, marginBottom: Spacing.sm, textAlign: 'center', }, modalMessage: { fontSize: FontSizes.base, color: AppColors.textSecondary, textAlign: 'center', marginBottom: Spacing.lg, lineHeight: 22, }, 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', }, // Debug Panel styles debugPanel: { backgroundColor: '#1a1a2e', maxHeight: 200, borderBottomWidth: 2, borderBottomColor: '#e94560', }, debugHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 8, backgroundColor: '#16213e', }, debugTitle: { color: '#e94560', fontWeight: 'bold', fontSize: 14, }, debugButtons: { flexDirection: 'row', gap: 8, }, debugCopyBtn: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#0f3460', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 4, gap: 4, }, debugClearBtn: { backgroundColor: '#e94560', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 4, }, debugCloseBtn: { backgroundColor: '#333', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, }, debugBtnText: { color: '#fff', fontSize: 12, fontWeight: '600', }, debugScroll: { padding: 8, }, debugLog: { color: '#00ff88', fontSize: 11, fontFamily: 'monospace', marginBottom: 2, }, debugEmpty: { color: '#666', fontSize: 12, fontStyle: 'italic', }, });