Compare commits

..

No commits in common. "01bebeedbe33bda8e71ffdc27cb513bd4fc024ea" and "06802c237b09af2f2f5d4f16d53eadd9d7d911a1" have entirely different histories.

4 changed files with 15 additions and 184 deletions

View File

@ -7,8 +7,6 @@ import {
Alert,
ActivityIndicator,
Modal,
ScrollView,
Clipboard,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
@ -19,14 +17,10 @@ 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 }>();
@ -37,16 +31,6 @@ 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();
@ -55,25 +39,16 @@ export default function SubscriptionScreen() {
}, [id]);
const loadBeneficiary = async () => {
if (!id) {
addDebugLog(`loadBeneficiary: no id provided`);
return;
}
addDebugLog(`loadBeneficiary: fetching id=${id}`);
if (!id) return;
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);
@ -113,11 +88,7 @@ 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;
}
@ -127,34 +98,27 @@ 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(requestBody),
body: JSON.stringify({
beneficiaryId: beneficiary.id,
}),
});
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);
@ -166,7 +130,6 @@ export default function SubscriptionScreen() {
}
const data = await response.json();
addDebugLog(`handleSubscribe: response data=${JSON.stringify(data)}`);
// Check if already subscribed
if (data.alreadySubscribed) {
@ -228,9 +191,9 @@ export default function SubscriptionScreen() {
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...');
// 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...');
const confirmResponse = await fetch(
`${STRIPE_API_URL}/confirm-subscription-payment`,
{
@ -240,19 +203,13 @@ export default function SubscriptionScreen() {
'Content-Type': 'application/json',
},
body: JSON.stringify({
setupIntentId: data.setupIntentId,
beneficiaryId: beneficiary.id,
subscriptionId: data.subscriptionId,
}),
}
);
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}`,
@ -435,60 +392,15 @@ 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>
<TouchableOpacity onPress={() => setShowDebugPanel(!showDebugPanel)}>
<Ionicons name="bug" size={24} color={showDebugPanel ? AppColors.primary : AppColors.textMuted} />
</TouchableOpacity>
<View style={styles.placeholder} />
</View>
<View style={styles.content}>
@ -845,67 +757,4 @@ 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',
},
});

View File

@ -354,7 +354,8 @@ router.post('/', async (req, res) => {
id: invitation.id,
code: invitation.token,
role: role,
email: email || null
email: email || null,
expiresAt: invitation.expires_at
}
});
@ -388,7 +389,7 @@ router.get('/beneficiary/:beneficiaryId', async (req, res) => {
// Get invitations for this beneficiary
const { data: invitations, error } = await supabase
.from('invitations')
.select('id, token, role, email, label, accepted_at, accepted_by, created_at')
.select('id, token, role, email, label, expires_at, accepted_at, accepted_by, created_at')
.eq('beneficiary_id', beneficiaryId)
.order('created_at', { ascending: false });
@ -416,6 +417,7 @@ router.get('/beneficiary/:beneficiaryId', async (req, res) => {
role: inv.role,
email: inv.email,
label: inv.label,
expiresAt: inv.expires_at,
acceptedAt: inv.accepted_at,
createdAt: inv.created_at,
acceptedBy: acceptedUser ? {
@ -559,7 +561,7 @@ router.get('/', async (req, res) => {
// Get invitations
const { data: invitations, error } = await supabase
.from('invitations')
.select('id, token, role, email, label, beneficiary_id, accepted_at, created_at')
.select('id, token, role, email, label, beneficiary_id, expires_at, accepted_at, created_at')
.eq('invited_by', userId)
.order('created_at', { ascending: false });
@ -583,6 +585,7 @@ router.get('/', async (req, res) => {
role: inv.role,
email: inv.email,
label: inv.label,
expiresAt: inv.expires_at,
acceptedAt: inv.accepted_at,
createdAt: inv.created_at,
beneficiary: beneficiary ? {

20
package-lock.json generated
View File

@ -42,7 +42,6 @@
"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",
@ -18813,25 +18812,6 @@
"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",

View File

@ -45,7 +45,6 @@
"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",