From e7831327bd331ae57fac472fa385c1daf8d725e4 Mon Sep 17 00:00:00 2001 From: Sergei Date: Thu, 8 Jan 2026 22:16:22 -0800 Subject: [PATCH] Fix Stripe subscription flow - use SetupIntent - Changed from PaymentIntent to SetupIntent flow - SetupIntent always has client_secret (unlike incomplete subscriptions) - Two-step process: collect payment method, then create subscription - Added debug panel for troubleshooting (DEBUG_MODE=true) - Updated price to $49/month (price_1SnYfkP0gvUw6M9C1095uFgW) - Added react-native-root-toast dependency Server changes (on 91.98.205.156): - /create-subscription-payment-sheet: returns SetupIntent instead of subscription - /confirm-subscription-payment: creates subscription with saved payment method - Added safeTimestampToISO() to prevent "Invalid time value" errors --- .../beneficiaries/[id]/subscription.tsx | 169 +++++++++++++++++- package-lock.json | 20 +++ package.json | 1 + 3 files changed, 181 insertions(+), 9 deletions(-) diff --git a/app/(tabs)/beneficiaries/[id]/subscription.tsx b/app/(tabs)/beneficiaries/[id]/subscription.tsx index d3fcdc7..6810ff5 100644 --- a/app/(tabs)/beneficiaries/[id]/subscription.tsx +++ b/app/(tabs)/beneficiaries/[id]/subscription.tsx @@ -7,6 +7,8 @@ import { Alert, ActivityIndicator, Modal, + ScrollView, + Clipboard, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -17,10 +19,14 @@ 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 }>(); @@ -31,6 +37,16 @@ export default function SubscriptionScreen() { 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(); @@ -39,16 +55,25 @@ export default function SubscriptionScreen() { }, [id]); const loadBeneficiary = async () => { - if (!id) return; + 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); @@ -88,7 +113,11 @@ export default function SubscriptionScreen() { }, [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; } @@ -98,27 +127,34 @@ export default function SubscriptionScreen() { 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({ - beneficiaryId: beneficiary.id, - }), + 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); @@ -130,6 +166,7 @@ export default function SubscriptionScreen() { } const data = await response.json(); + addDebugLog(`handleSubscribe: response data=${JSON.stringify(data)}`); // Check if already subscribed if (data.alreadySubscribed) { @@ -191,9 +228,9 @@ export default function SubscriptionScreen() { throw new Error(presentError.message); } - // 4. Payment successful! Confirm the subscription payment - // This ensures the subscription invoice gets paid if using manual PaymentIntent - console.log('[Subscription] Payment Sheet completed, confirming payment...'); + // 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`, { @@ -203,13 +240,19 @@ export default function SubscriptionScreen() { 'Content-Type': 'application/json', }, body: JSON.stringify({ - subscriptionId: data.subscriptionId, + 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}`, @@ -392,15 +435,60 @@ export default function SubscriptionScreen() { ); } + 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)}> + + @@ -757,4 +845,67 @@ const styles = StyleSheet.create({ 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', + }, }); diff --git a/package-lock.json b/package-lock.json index 54082a6..ad39780 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.28.0", "react-native-reanimated": "~4.1.1", + "react-native-root-toast": "^4.0.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-sherpa-onnx-offline-tts": "^0.2.4", @@ -18812,6 +18813,25 @@ "node": ">=10" } }, + "node_modules/react-native-root-siblings": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-native-root-siblings/-/react-native-root-siblings-5.0.1.tgz", + "integrity": "sha512-Ay3k/fBj6ReUkWX5WNS+oEAcgPLEGOK8n7K/L7D85mf3xvd8rm/b4spsv26E4HlFzluVx5HKbxEt9cl0wQ1u3g==", + "license": "MIT" + }, + "node_modules/react-native-root-toast": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-native-root-toast/-/react-native-root-toast-4.0.1.tgz", + "integrity": "sha512-Zpz+EMKfcCAO/+KuarPeJ/BkmMrXYWRPyTeBhvUondykqUOkQqQO8DEbxqwoUv+ax7MbS3cDJW94L4qTbOFtBw==", + "license": "MIT", + "dependencies": { + "react-native-root-siblings": "^5.0.0" + }, + "peerDependencies": { + "react-native": ">=0.47.0", + "react-native-safe-area-context": "*" + } + }, "node_modules/react-native-safe-area-context": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", diff --git a/package.json b/package.json index cef1ce6..2edb523 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.28.0", "react-native-reanimated": "~4.1.1", + "react-native-root-toast": "^4.0.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-sherpa-onnx-offline-tts": "^0.2.4",