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
This commit is contained in:
parent
06802c237b
commit
e7831327bd
@ -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<string[]>([]);
|
||||
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 (
|
||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||
{/* Debug Panel */}
|
||||
{showDebugPanel && (
|
||||
<View style={styles.debugPanel}>
|
||||
<View style={styles.debugHeader}>
|
||||
<Text style={styles.debugTitle}>🐛 Debug Panel</Text>
|
||||
<View style={styles.debugButtons}>
|
||||
<TouchableOpacity onPress={copyDebugLogs} style={styles.debugCopyBtn}>
|
||||
<Ionicons name="copy" size={16} color="#fff" />
|
||||
<Text style={styles.debugBtnText}>Copy</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setDebugLogs([])} style={styles.debugClearBtn}>
|
||||
<Text style={styles.debugBtnText}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setShowDebugPanel(false)} style={styles.debugCloseBtn}>
|
||||
<Ionicons name="close" size={16} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<ScrollView style={styles.debugScroll}>
|
||||
{debugLogs.length === 0 ? (
|
||||
<Text style={styles.debugEmpty}>No logs yet. Press Subscribe to see logs.</Text>
|
||||
) : (
|
||||
debugLogs.map((log, i) => (
|
||||
<Text key={i} style={styles.debugLog}>{log}</Text>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 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} />
|
||||
<TouchableOpacity onPress={() => setShowDebugPanel(!showDebugPanel)}>
|
||||
<Ionicons name="bug" size={24} color={showDebugPanel ? AppColors.primary : AppColors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.content}>
|
||||
@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user