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,
|
Alert,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Modal,
|
Modal,
|
||||||
|
ScrollView,
|
||||||
|
Clipboard,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
@ -17,10 +19,14 @@ import { useAuth } from '@/contexts/AuthContext';
|
|||||||
import { api } from '@/services/api';
|
import { api } from '@/services/api';
|
||||||
import { hasBeneficiaryDevices } from '@/services/BeneficiaryDetailController';
|
import { hasBeneficiaryDevices } from '@/services/BeneficiaryDetailController';
|
||||||
import type { Beneficiary } from '@/types';
|
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 STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe';
|
||||||
const SUBSCRIPTION_PRICE = 49; // $49/month
|
const SUBSCRIPTION_PRICE = 49; // $49/month
|
||||||
|
|
||||||
|
// DEBUG MODE - set to true to show debug panel
|
||||||
|
const DEBUG_MODE = true;
|
||||||
|
|
||||||
export default function SubscriptionScreen() {
|
export default function SubscriptionScreen() {
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
@ -31,6 +37,16 @@ export default function SubscriptionScreen() {
|
|||||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||||
const [justSubscribed, setJustSubscribed] = useState(false); // Prevent self-guard redirect after payment
|
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 { initPaymentSheet, presentPaymentSheet } = usePaymentSheet();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
@ -39,16 +55,25 @@ export default function SubscriptionScreen() {
|
|||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const loadBeneficiary = async () => {
|
const loadBeneficiary = async () => {
|
||||||
if (!id) return;
|
if (!id) {
|
||||||
|
addDebugLog(`loadBeneficiary: no id provided`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDebugLog(`loadBeneficiary: fetching id=${id}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.getWellNuoBeneficiary(parseInt(id, 10));
|
const response = await api.getWellNuoBeneficiary(parseInt(id, 10));
|
||||||
|
addDebugLog(`loadBeneficiary: response.ok=${response.ok}`);
|
||||||
if (response.ok && response.data) {
|
if (response.ok && response.data) {
|
||||||
setBeneficiary(response.data);
|
setBeneficiary(response.data);
|
||||||
|
addDebugLog(`loadBeneficiary: got beneficiary id=${response.data.id}, name=${response.data.name}`);
|
||||||
} else {
|
} else {
|
||||||
|
addDebugLog(`loadBeneficiary: ERROR - ${response.error}`);
|
||||||
console.error('Failed to load beneficiary:', response.error);
|
console.error('Failed to load beneficiary:', response.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
addDebugLog(`loadBeneficiary: EXCEPTION - ${error}`);
|
||||||
console.error('Failed to load beneficiary:', error);
|
console.error('Failed to load beneficiary:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -88,7 +113,11 @@ export default function SubscriptionScreen() {
|
|||||||
}, [beneficiary, isLoading, id, isActive, justSubscribed, showSuccessModal]);
|
}, [beneficiary, isLoading, id, isActive, justSubscribed, showSuccessModal]);
|
||||||
|
|
||||||
const handleSubscribe = async () => {
|
const handleSubscribe = async () => {
|
||||||
|
addDebugLog(`handleSubscribe: START`);
|
||||||
|
addDebugLog(`handleSubscribe: beneficiary=${JSON.stringify(beneficiary ? {id: beneficiary.id, name: beneficiary.name} : null)}`);
|
||||||
|
|
||||||
if (!beneficiary) {
|
if (!beneficiary) {
|
||||||
|
addDebugLog(`handleSubscribe: ERROR - no beneficiary`);
|
||||||
Alert.alert('Error', 'Beneficiary data not loaded.');
|
Alert.alert('Error', 'Beneficiary data not loaded.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -98,27 +127,34 @@ export default function SubscriptionScreen() {
|
|||||||
try {
|
try {
|
||||||
// Get auth token
|
// Get auth token
|
||||||
const token = await api.getToken();
|
const token = await api.getToken();
|
||||||
|
addDebugLog(`handleSubscribe: token=${token ? token.substring(0, 20) + '...' : 'null'}`);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
addDebugLog(`handleSubscribe: ERROR - no token`);
|
||||||
Alert.alert('Error', 'Please log in again');
|
Alert.alert('Error', 'Please log in again');
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Create subscription payment sheet via Stripe Subscriptions API
|
// 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`, {
|
const response = await fetch(`${STRIPE_API_URL}/create-subscription-payment-sheet`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(requestBody),
|
||||||
beneficiaryId: beneficiary.id,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addDebugLog(`handleSubscribe: response.status=${response.status}`);
|
||||||
|
|
||||||
// Check if response is OK before parsing JSON
|
// Check if response is OK before parsing JSON
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
|
addDebugLog(`handleSubscribe: ERROR response - ${errorText}`);
|
||||||
let errorMessage = 'Failed to create payment';
|
let errorMessage = 'Failed to create payment';
|
||||||
try {
|
try {
|
||||||
const errorJson = JSON.parse(errorText);
|
const errorJson = JSON.parse(errorText);
|
||||||
@ -130,6 +166,7 @@ export default function SubscriptionScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
addDebugLog(`handleSubscribe: response data=${JSON.stringify(data)}`);
|
||||||
|
|
||||||
// Check if already subscribed
|
// Check if already subscribed
|
||||||
if (data.alreadySubscribed) {
|
if (data.alreadySubscribed) {
|
||||||
@ -191,9 +228,9 @@ export default function SubscriptionScreen() {
|
|||||||
throw new Error(presentError.message);
|
throw new Error(presentError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Payment successful! Confirm the subscription payment
|
// 4. Payment successful! Create subscription with the payment method from SetupIntent
|
||||||
// This ensures the subscription invoice gets paid if using manual PaymentIntent
|
addDebugLog(`handleSubscribe: PaymentSheet completed, creating subscription...`);
|
||||||
console.log('[Subscription] Payment Sheet completed, confirming payment...');
|
console.log('[Subscription] Payment Sheet completed, creating subscription...');
|
||||||
const confirmResponse = await fetch(
|
const confirmResponse = await fetch(
|
||||||
`${STRIPE_API_URL}/confirm-subscription-payment`,
|
`${STRIPE_API_URL}/confirm-subscription-payment`,
|
||||||
{
|
{
|
||||||
@ -203,13 +240,19 @@ export default function SubscriptionScreen() {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
subscriptionId: data.subscriptionId,
|
setupIntentId: data.setupIntentId,
|
||||||
|
beneficiaryId: beneficiary.id,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const confirmData = await confirmResponse.json();
|
const confirmData = await confirmResponse.json();
|
||||||
|
addDebugLog(`handleSubscribe: confirm response=${JSON.stringify(confirmData)}`);
|
||||||
console.log('[Subscription] Confirm response:', 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
|
// 5. Fetch subscription status from Stripe
|
||||||
const statusResponse = await fetch(
|
const statusResponse = await fetch(
|
||||||
`${STRIPE_API_URL}/subscription-status/${beneficiary.id}`,
|
`${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 (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
<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 */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
||||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>Subscription</Text>
|
<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>
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
@ -757,4 +845,67 @@ const styles = StyleSheet.create({
|
|||||||
color: AppColors.white,
|
color: AppColors.white,
|
||||||
textAlign: 'center',
|
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-fs": "^2.20.0",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
|
"react-native-root-toast": "^4.0.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-sherpa-onnx-offline-tts": "^0.2.4",
|
"react-native-sherpa-onnx-offline-tts": "^0.2.4",
|
||||||
@ -18812,6 +18813,25 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/react-native-safe-area-context": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
"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-fs": "^2.20.0",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
|
"react-native-root-toast": "^4.0.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-sherpa-onnx-offline-tts": "^0.2.4",
|
"react-native-sherpa-onnx-offline-tts": "^0.2.4",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user