diff --git a/app/(auth)/purchase.tsx b/app/(auth)/purchase.tsx index a927d9b..1a03277 100644 --- a/app/(auth)/purchase.tsx +++ b/app/(auth)/purchase.tsx @@ -1,10 +1,9 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, - ScrollView, ActivityIndicator, Alert, } from 'react-native'; @@ -15,6 +14,8 @@ import { usePaymentSheet } from '@stripe/stripe-react-native'; import { AppColors, Spacing, BorderRadius, FontSizes, FontWeights, Shadows } from '@/constants/theme'; import { useAuth } from '@/contexts/AuthContext'; import { api } from '@/services/api'; +import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; +import { hasBeneficiaryDevices } from '@/services/BeneficiaryDetailController'; const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe'; @@ -22,34 +23,58 @@ const STARTER_KIT = { name: 'WellNuo Starter Kit', price: '$249', priceValue: 249, - description: 'Everything you need to start monitoring your loved ones', - features: [ - 'Motion sensor (PIR)', - 'Door/window sensor', - 'Temperature & humidity sensor', - 'WellNuo Hub', - 'Mobile app access', - '1 year subscription included', - ], }; export default function PurchaseScreen() { - // Get lovedOneName from add-loved-one flow const params = useLocalSearchParams<{ lovedOneName?: string; beneficiaryId?: string }>(); const lovedOneName = params.lovedOneName || ''; const beneficiaryId = params.beneficiaryId; const [isProcessing, setIsProcessing] = useState(false); + const [isLoading, setIsLoading] = useState(true); const [step, setStep] = useState<'purchase' | 'order_placed'>('purchase'); const { user } = useAuth(); const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet(); + // Check if equipment is already ordered - redirect to equipment-status + const checkEquipmentStatus = useCallback(async () => { + if (!beneficiaryId) { + setIsLoading(false); + return; + } + + try { + const response = await api.getWellNuoBeneficiary(parseInt(beneficiaryId, 10)); + if (response.ok && response.data) { + // If user already has devices - go to main screen + if (hasBeneficiaryDevices(response.data)) { + router.replace(`/(tabs)/beneficiaries/${beneficiaryId}`); + return; + } + + // If equipment is ordered/shipped/delivered - go to equipment-status + const status = response.data.equipmentStatus; + if (status && ['ordered', 'shipped', 'delivered'].includes(status)) { + router.replace(`/(tabs)/beneficiaries/${beneficiaryId}/equipment-status`); + return; + } + } + } catch (error) { + console.warn('[Purchase] Failed to check equipment status:', error); + } + + setIsLoading(false); + }, [beneficiaryId]); + + useEffect(() => { + checkEquipmentStatus(); + }, [checkEquipmentStatus]); + const handlePurchase = async () => { setIsProcessing(true); try { - // Validate required data before proceeding const userId = user?.user_id; if (!userId) { Alert.alert('Error', 'User not authenticated. Please log in again.'); @@ -63,7 +88,6 @@ export default function PurchaseScreen() { return; } - // Get auth token const token = await api.getToken(); if (!token) { Alert.alert('Error', 'Please log in again'); @@ -73,7 +97,6 @@ export default function PurchaseScreen() { console.log('[Purchase] Creating payment sheet for userId:', userId, 'beneficiaryId:', beneficiaryId); - // 1. Create Payment Sheet on our server const response = await fetch(`${STRIPE_API_URL}/create-payment-sheet`, { method: 'POST', headers: { @@ -82,7 +105,7 @@ export default function PurchaseScreen() { }, body: JSON.stringify({ email: user?.email, - amount: STARTER_KIT.priceValue * 100, // Convert to cents ($249.00) + amount: STARTER_KIT.priceValue * 100, metadata: { userId: String(userId), beneficiaryId: String(beneficiaryId), @@ -98,7 +121,6 @@ export default function PurchaseScreen() { throw new Error(data.error || 'Failed to create payment sheet'); } - // 2. Initialize the Payment Sheet const { error: initError } = await initPaymentSheet({ merchantDisplayName: 'WellNuo', paymentIntentClientSecret: data.paymentIntent, @@ -121,19 +143,16 @@ export default function PurchaseScreen() { throw new Error(initError.message); } - // 3. Present the Payment Sheet const { error: presentError } = await presentPaymentSheet(); if (presentError) { if (presentError.code === 'Canceled') { - // User cancelled - do nothing setIsProcessing(false); return; } throw new Error(presentError.message); } - // 4. Payment successful! Update equipment status to 'ordered' console.log('[Purchase] Payment successful, updating equipment status...'); const statusResponse = await api.updateBeneficiaryEquipmentStatus( parseInt(beneficiaryId, 10), @@ -142,13 +161,9 @@ export default function PurchaseScreen() { if (!statusResponse.ok) { console.warn('[Purchase] Failed to update equipment status:', statusResponse.error?.message); - // Continue anyway - payment was successful } - // Mark onboarding as completed await api.setOnboardingCompleted(true); - - // Show Order Placed screen setStep('order_placed'); } catch (error) { console.error('Payment error:', error); @@ -161,8 +176,7 @@ export default function PurchaseScreen() { setIsProcessing(false); }; - const handleSkip = () => { - // User says "I already have a kit" - go to activate screen with beneficiary ID + const handleAlreadyHaveSensors = () => { router.replace({ pathname: '/(auth)/activate', params: { beneficiaryId, lovedOneName }, @@ -177,12 +191,16 @@ export default function PurchaseScreen() { } }; + // Loading state - checking equipment status + if (isLoading) { + return ; + } + // Order Placed Screen if (step === 'order_placed') { return ( - {/* Success Icon */} @@ -192,7 +210,6 @@ export default function PurchaseScreen() { Thank you for your purchase - {/* Order Info */} Item @@ -202,53 +219,15 @@ export default function PurchaseScreen() { For {lovedOneName || 'Your loved one'} - + Total {STARTER_KIT.price} - {/* What's Next */} - - What's Next? - - - - 1 - - - Order Processing - We'll prepare your kit for shipping - - - - - - 2 - - - Shipping Notification - You'll receive an email with tracking info - - - - - - 3 - - - Activate Your Kit - Enter the serial number when delivered - - - - - {/* Actions */} - - - Track My Kit - - + + Track My Order + ); @@ -256,7 +235,7 @@ export default function PurchaseScreen() { return ( - + {/* Header */} + {STARTER_KIT.name} {STARTER_KIT.price} - {STARTER_KIT.description} - {/* Features */} - - {STARTER_KIT.features.map((feature, index) => ( - - - {feature} - - ))} + + 4 smart sensors that easily plug into any outlet and set up through the app in minutes + + + {/* Security Badge */} + + + Secure payment powered by Stripe - {/* Security Badge */} - - - - Secure payment powered by Stripe - + {/* Bottom Actions */} + + + {isProcessing ? ( + + ) : ( + <> + + Buy Now - {STARTER_KIT.price} + + )} + + + + I already have sensors + - - - {/* Bottom Actions */} - - - {isProcessing ? ( - - ) : ( - <> - - Buy Now - {STARTER_KIT.price} - - )} - - - - I already have a kit - ); @@ -329,13 +300,14 @@ const styles = StyleSheet.create({ backgroundColor: AppColors.background, }, content: { + flex: 1, padding: Spacing.lg, + justifyContent: 'space-between', }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - marginBottom: Spacing.xl, }, backButton: { padding: Spacing.sm, @@ -356,11 +328,7 @@ const styles = StyleSheet.create({ alignItems: 'center', borderWidth: 2, borderColor: AppColors.primary, - shadowColor: '#000', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.1, - shadowRadius: 12, - elevation: 5, + ...Shadows.md, }, productIcon: { width: 80, @@ -372,45 +340,31 @@ const styles = StyleSheet.create({ marginBottom: Spacing.lg, }, productName: { - fontSize: FontSizes['2xl'], + fontSize: FontSizes.xl, fontWeight: FontWeights.bold, color: AppColors.textPrimary, textAlign: 'center', - marginBottom: Spacing.sm, + marginBottom: Spacing.xs, }, productPrice: { fontSize: FontSizes['3xl'], fontWeight: FontWeights.bold, color: AppColors.primary, - marginBottom: Spacing.sm, + marginBottom: Spacing.lg, }, productDescription: { fontSize: FontSizes.base, color: AppColors.textSecondary, textAlign: 'center', - marginBottom: Spacing.xl, - }, - features: { - width: '100%', - gap: Spacing.md, - }, - featureRow: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.sm, - }, - featureText: { - fontSize: FontSizes.base, - color: AppColors.textPrimary, - flex: 1, + lineHeight: 22, + marginBottom: Spacing.lg, }, securityBadge: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'center', - gap: Spacing.sm, - marginTop: Spacing.xl, - paddingVertical: Spacing.md, + gap: Spacing.xs, + paddingVertical: Spacing.sm, + paddingHorizontal: Spacing.md, backgroundColor: `${AppColors.success}10`, borderRadius: BorderRadius.lg, }, @@ -419,8 +373,6 @@ const styles = StyleSheet.create({ color: AppColors.success, }, bottomActions: { - padding: Spacing.lg, - paddingBottom: Spacing.xl, gap: Spacing.md, }, purchaseButton: { @@ -431,6 +383,7 @@ const styles = StyleSheet.create({ backgroundColor: AppColors.primary, paddingVertical: Spacing.lg, borderRadius: BorderRadius.lg, + ...Shadows.primary, }, buttonDisabled: { opacity: 0.7, @@ -442,14 +395,14 @@ const styles = StyleSheet.create({ }, skipButton: { alignItems: 'center', - paddingVertical: Spacing.md, + paddingVertical: Spacing.sm, }, skipButtonText: { fontSize: FontSizes.base, color: AppColors.textSecondary, textDecorationLine: 'underline', }, - // Order Placed Screen Styles + // Order Placed Screen orderPlacedContainer: { flex: 1, padding: Spacing.lg, @@ -482,7 +435,7 @@ const styles = StyleSheet.create({ backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, padding: Spacing.lg, - marginBottom: Spacing.lg, + marginBottom: Spacing.xl, ...Shadows.sm, }, orderInfoRow: { @@ -506,58 +459,8 @@ const styles = StyleSheet.create({ color: AppColors.primary, fontWeight: FontWeights.bold, }, - whatsNextCard: { - width: '100%', - backgroundColor: AppColors.surface, - borderRadius: BorderRadius.lg, - padding: Spacing.lg, - marginBottom: Spacing.xl, - ...Shadows.sm, - }, - whatsNextTitle: { - fontSize: FontSizes.lg, - fontWeight: FontWeights.semibold, - color: AppColors.textPrimary, - marginBottom: Spacing.lg, - }, - stepItem: { - flexDirection: 'row', - marginBottom: Spacing.md, - }, - stepNumber: { - width: 28, - height: 28, - borderRadius: 14, - backgroundColor: AppColors.primary, - alignItems: 'center', - justifyContent: 'center', - marginRight: Spacing.md, - }, - stepNumberLast: { - backgroundColor: AppColors.textMuted, - }, - stepNumberText: { - fontSize: FontSizes.sm, - fontWeight: FontWeights.bold, - color: AppColors.white, - }, - stepContent: { - flex: 1, - }, - stepTitle: { - fontSize: FontSizes.base, - fontWeight: FontWeights.medium, - color: AppColors.textPrimary, - }, - stepDescription: { - fontSize: FontSizes.sm, - color: AppColors.textSecondary, - marginTop: 2, - }, - orderPlacedActions: { - width: '100%', - }, primaryButton: { + width: '100%', backgroundColor: AppColors.primary, paddingVertical: Spacing.lg, borderRadius: BorderRadius.lg, diff --git a/app/(tabs)/beneficiaries/[id]/equipment-status.tsx b/app/(tabs)/beneficiaries/[id]/equipment-status.tsx index 6fcd859..e29e262 100644 --- a/app/(tabs)/beneficiaries/[id]/equipment-status.tsx +++ b/app/(tabs)/beneficiaries/[id]/equipment-status.tsx @@ -191,7 +191,7 @@ export default function EquipmentStatusScreen() { {/* Header */} - router.back()}> + router.replace('/(tabs)')}> {beneficiary.name} diff --git a/app/(tabs)/beneficiaries/[id]/index.tsx b/app/(tabs)/beneficiaries/[id]/index.tsx index 72392e9..7f1c8ae 100644 --- a/app/(tabs)/beneficiaries/[id]/index.tsx +++ b/app/(tabs)/beneficiaries/[id]/index.tsx @@ -64,8 +64,6 @@ export default function BeneficiaryDetailScreen() { const [showWebView, setShowWebView] = useState(false); const [isWebViewReady, setIsWebViewReady] = useState(false); const [authToken, setAuthToken] = useState(null); - const [userName, setUserName] = useState(null); - const [userId, setUserId] = useState(null); // Edit modal state const [isEditModalVisible, setIsEditModalVisible] = useState(false); @@ -78,11 +76,7 @@ export default function BeneficiaryDetailScreen() { const loadLegacyToken = async () => { try { const token = await api.getLegacyToken(); - const name = await api.getLegacyUserName(); - const id = await api.getLegacyUserId(); setAuthToken(token); - setUserName(name); - setUserId(id); setIsWebViewReady(true); } catch (err) { console.log('[BeneficiaryDetail] Legacy token not available'); @@ -238,12 +232,10 @@ export default function BeneficiaryDetailScreen() { (function() { try { var authData = { - username: '${userName || ''}', - token: '${authToken}', - user_id: ${userId || 'null'} + token: '${authToken}' }; localStorage.setItem('auth2', JSON.stringify(authData)); - console.log('Auth data injected:', authData.username); + console.log('Auth data injected'); } catch(e) { console.error('Failed to inject token:', e); } diff --git a/app/(tabs)/beneficiaries/[id]/purchase.tsx b/app/(tabs)/beneficiaries/[id]/purchase.tsx index 59090ad..373a096 100644 --- a/app/(tabs)/beneficiaries/[id]/purchase.tsx +++ b/app/(tabs)/beneficiaries/[id]/purchase.tsx @@ -34,14 +34,7 @@ const STARTER_KIT = { name: 'WellNuo Starter Kit', price: '$249', priceValue: 249, - features: [ - 'Motion sensor (PIR)', - 'Door/window sensor', - 'Temperature & humidity sensor', - 'WellNuo Hub', - 'Mobile app access', - '1 year subscription included', - ], + description: '4 smart sensors that easily plug into any outlet and set up through the app in minutes', }; export default function PurchaseScreen() { @@ -186,28 +179,6 @@ export default function PurchaseScreen() { }); }; - const handleTryDemo = async () => { - if (!beneficiary || !id) return; - - setIsProcessing(true); - - try { - // Create demo device via API - const response = await api.activateDemoDevice(parseInt(id, 10)); - - if (response.ok) { - toast.success('Demo Activated!', 'You can now explore the dashboard.'); - router.replace(`/(tabs)/beneficiaries/${id}/subscription`); - } else { - toast.error('Error', response.error?.message || 'Failed to activate demo'); - } - } catch (error) { - toast.error('Error', 'Failed to activate demo mode'); - } finally { - setIsProcessing(false); - } - }; - if (isLoading) { return ; } @@ -253,15 +224,17 @@ export default function PurchaseScreen() { {STARTER_KIT.name} {STARTER_KIT.price} - - {STARTER_KIT.features.map((feature, index) => ( - - - {feature} - - ))} - + {STARTER_KIT.description} + {/* Security Badge */} + + + Secure payment by Stripe + + + + {/* Actions */} + - {/* Security Badge */} - - - Secure payment by Stripe - - - - {/* Alternative Options */} - - - - I already have sensors - - - - - - Try demo mode - + + I already have sensors @@ -379,19 +335,14 @@ const styles = StyleSheet.create({ textAlign: 'center', marginVertical: Spacing.md, }, - features: { - marginBottom: Spacing.lg, + kitDescription: { + fontSize: FontSizes.base, + color: AppColors.textSecondary, + textAlign: 'center', + lineHeight: 22, }, - featureRow: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: Spacing.xs, - gap: Spacing.sm, - }, - featureText: { - fontSize: FontSizes.sm, - color: AppColors.textPrimary, - flex: 1, + actionsSection: { + gap: Spacing.md, }, buyButton: { flexDirection: 'row', @@ -421,20 +372,13 @@ const styles = StyleSheet.create({ fontSize: FontSizes.xs, color: AppColors.success, }, - alternativeSection: { - gap: Spacing.sm, - }, - alternativeButton: { - flexDirection: 'row', + alreadyHaveButton: { alignItems: 'center', - backgroundColor: AppColors.surface, - padding: Spacing.md, - borderRadius: BorderRadius.lg, - gap: Spacing.md, + paddingVertical: Spacing.sm, }, - alternativeText: { - flex: 1, + alreadyHaveText: { fontSize: FontSizes.base, - color: AppColors.textPrimary, + color: AppColors.textSecondary, + textDecorationLine: 'underline', }, }); diff --git a/app/(tabs)/beneficiaries/[id]/subscription.tsx b/app/(tabs)/beneficiaries/[id]/subscription.tsx index 316ab24..d3fcdc7 100644 --- a/app/(tabs)/beneficiaries/[id]/subscription.tsx +++ b/app/(tabs)/beneficiaries/[id]/subscription.tsx @@ -3,10 +3,10 @@ import { View, Text, StyleSheet, - ScrollView, TouchableOpacity, Alert, ActivityIndicator, + Modal, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -21,25 +21,6 @@ import type { Beneficiary } from '@/types'; const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe'; const SUBSCRIPTION_PRICE = 49; // $49/month -interface PlanFeatureProps { - text: string; - included: boolean; -} - -function PlanFeature({ text, included }: PlanFeatureProps) { - return ( - - - - {text} - - - ); -} export default function SubscriptionScreen() { const { id } = useLocalSearchParams<{ id: string }>(); @@ -47,6 +28,8 @@ export default function SubscriptionScreen() { 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 const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet(); const { user } = useAuth(); @@ -80,14 +63,13 @@ export default function SubscriptionScreen() { // Check if subscription is canceled but still active until period end const isCanceledButActive = isActive && subscription?.cancelAtPeriodEnd === true; - const daysRemaining = subscription?.endDate - ? Math.max(0, Math.ceil((new Date(subscription.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24))) - : 0; - // 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; @@ -103,7 +85,7 @@ export default function SubscriptionScreen() { if (isActive) { router.replace(`/(tabs)/beneficiaries/${id}`); } - }, [beneficiary, isLoading, id, isActive]); + }, [beneficiary, isLoading, id, isActive, justSubscribed, showSuccessModal]); const handleSubscribe = async () => { if (!beneficiary) { @@ -134,6 +116,19 @@ export default function SubscriptionScreen() { }), }); + // Check if response is OK before parsing JSON + if (!response.ok) { + const errorText = await response.text(); + 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(); // Check if already subscribed @@ -151,11 +146,17 @@ export default function SubscriptionScreen() { } // 2. Initialize the Payment Sheet - const { error: initError } = await initPaymentSheet({ + // Determine if clientSecret is for PaymentIntent (pi_) or SetupIntent (seti_) + const isSetupIntent = data.clientSecret.startsWith('seti_'); + + const paymentSheetParams: Parameters[0] = { merchantDisplayName: 'WellNuo', - paymentIntentClientSecret: data.clientSecret, customerId: data.customer, - customerEphemeralKeySecret: data.ephemeralKey, + // 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', @@ -164,7 +165,16 @@ export default function SubscriptionScreen() { 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); @@ -181,7 +191,26 @@ export default function SubscriptionScreen() { throw new Error(presentError.message); } - // 4. Payment successful! Fetch subscription status from Stripe + // 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`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + subscriptionId: data.subscriptionId, + }), + } + ); + const confirmData = await confirmResponse.json(); + console.log('[Subscription] Confirm response:', confirmData); + + // 5. Fetch subscription status from Stripe const statusResponse = await fetch( `${STRIPE_API_URL}/subscription-status/${beneficiary.id}`, { @@ -192,14 +221,14 @@ export default function SubscriptionScreen() { ); 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(); - Alert.alert( - 'Subscription Activated!', - `Subscription for ${beneficiary.name} is now active.`, - [{ text: 'Great!' }] - ); + // Show success modal instead of Alert + setShowSuccessModal(true); } catch (error) { console.error('Payment error:', error); Alert.alert( @@ -309,18 +338,6 @@ export default function SubscriptionScreen() { } }; - const handleRestorePurchases = () => { - Alert.alert( - 'Restoring Purchases', - 'Looking for previous purchases...', - [{ text: 'OK' }] - ); - - setTimeout(() => { - Alert.alert('No Purchases Found', 'We couldn\'t find any previous purchases associated with your account.'); - }, 1500); - }; - const formatDate = (date: Date) => { return date.toLocaleDateString('en-US', { year: 'numeric', @@ -329,6 +346,12 @@ export default function SubscriptionScreen() { }); }; + const handleSuccessModalClose = () => { + setShowSuccessModal(false); + // Navigate to beneficiary dashboard after closing modal + router.replace(`/(tabs)/beneficiaries/${id}`); + }; + if (isLoading) { return ( @@ -380,176 +403,126 @@ export default function SubscriptionScreen() { - - {/* Beneficiary Info */} - - - - {beneficiary.name.charAt(0).toUpperCase()} - - - - Subscription for - {beneficiary.name} - - - - {/* Current Status */} - - {isActive ? ( - isCanceledButActive ? ( - // Subscription canceled but still active until period end - <> - - - ENDING SOON - - Active until {subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'N/A'} - - {daysRemaining} - days left - - - ) : ( - // Active subscription - <> - - - ACTIVE - - Subscription is active - - Valid until {subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'N/A'} - - - {daysRemaining} - days remaining - - - ) - ) : ( - <> - - - - {subscription?.status === 'expired' ? 'EXPIRED' : 'NO SUBSCRIPTION'} - - - - {subscription?.status === 'expired' - ? 'Subscription has expired' - : `Subscribe for ${beneficiary.name}`} - - - Get full access to monitoring features - - - )} - - + {/* Subscription Card */} - - - - - - WellNuo - - - ${SUBSCRIPTION_PRICE} - /month - + + + 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'}`} + + )} - {/* Show Subscribe button when not active */} - {!isActive && ( - - {isProcessing ? ( - - ) : ( - <> - - - Subscribe — ${SUBSCRIPTION_PRICE}/month - - - )} - - )} - - {/* Show Reactivate button when subscription is canceled but still active */} - {isCanceledButActive && ( - - {isProcessing ? ( - - ) : ( - <> - - - Reactivate Subscription - - - )} - - )} - + {/* Security Badge */} + + + Secure payment by Stripe - {/* Secure Payment Badge */} - - - Secure payment powered by Stripe - + {/* Actions */} + + {/* Subscribe button when not active */} + {!isActive && ( + + {isProcessing ? ( + + ) : ( + <> + + + Subscribe + + + )} + + )} - {/* Links section */} - - - Restore Purchases - + {/* Reactivate button when canceled but still active */} + {isCanceledButActive && ( + + {isProcessing ? ( + + ) : ( + <> + + Reactivate Subscription + + )} + + )} + {/* Cancel link - only show when active subscription */} {isActive && !isCanceledButActive && ( - <> - - - {isCanceling ? ( - - ) : ( - Cancel - )} - - + + {isCanceling ? ( + + ) : ( + Cancel Subscription + )} + )} + - {/* Terms */} - - Payment will be charged to your account at the confirmation of purchase. - Subscription can be cancelled at any time from your account settings. - - + {/* Success Modal */} + + + + + + + Subscription Activated! + + Subscription for {beneficiary?.name} is now active. + + + Continue + + + + ); } @@ -557,7 +530,7 @@ export default function SubscriptionScreen() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: AppColors.surface, + backgroundColor: AppColors.background, }, header: { flexDirection: 'row', @@ -584,7 +557,6 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, - // No beneficiary state noBeneficiaryContainer: { flex: 1, justifyContent: 'center', @@ -612,171 +584,96 @@ const styles = StyleSheet.create({ textAlign: 'center', lineHeight: 24, }, - // Beneficiary banner - beneficiaryBanner: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: AppColors.primaryLighter, - marginHorizontal: Spacing.lg, - marginTop: Spacing.md, - padding: Spacing.md, - borderRadius: BorderRadius.lg, - }, - beneficiaryAvatar: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: AppColors.primary, + content: { + flex: 1, + padding: Spacing.lg, justifyContent: 'center', alignItems: 'center', }, - beneficiaryAvatarText: { - fontSize: FontSizes.xl, - fontWeight: FontWeights.bold, - color: AppColors.white, - }, - beneficiaryInfo: { - marginLeft: Spacing.md, - }, - beneficiaryLabel: { - fontSize: FontSizes.sm, - color: AppColors.textSecondary, - }, - beneficiaryName: { - fontSize: FontSizes.lg, - fontWeight: FontWeights.bold, - color: AppColors.textPrimary, - }, - // Status banner - statusBanner: { - backgroundColor: AppColors.background, - padding: Spacing.xl, - alignItems: 'center', - }, - activeBadge: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#D1FAE5', - paddingHorizontal: Spacing.md, - paddingVertical: Spacing.xs, - borderRadius: BorderRadius.full, - gap: Spacing.xs, - }, - activeBadgeText: { - fontSize: FontSizes.sm, - fontWeight: FontWeights.bold, - color: AppColors.success, - }, - inactiveBadge: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#FEE2E2', - paddingHorizontal: Spacing.md, - paddingVertical: Spacing.xs, - borderRadius: BorderRadius.full, - gap: Spacing.xs, - }, - inactiveBadgeText: { - fontSize: FontSizes.sm, - fontWeight: FontWeights.bold, - color: AppColors.error, - }, - cancelingBadge: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#FEF3C7', - paddingHorizontal: Spacing.md, - paddingVertical: Spacing.xs, - borderRadius: BorderRadius.full, - gap: Spacing.xs, - }, - cancelingBadgeText: { - fontSize: FontSizes.sm, - fontWeight: FontWeights.bold, - color: AppColors.warning, - }, - statusTitle: { - fontSize: FontSizes.xl, - fontWeight: FontWeights.bold, - color: AppColors.textPrimary, - marginTop: Spacing.md, - textAlign: 'center', - }, - statusDescription: { - fontSize: FontSizes.base, - color: AppColors.textSecondary, - marginTop: Spacing.xs, - textAlign: 'center', - }, - daysRemaining: { - marginTop: Spacing.lg, - alignItems: 'center', - }, - daysRemainingNumber: { - fontSize: 48, - fontWeight: FontWeights.bold, - color: AppColors.primary, - }, - daysRemainingLabel: { - fontSize: FontSizes.sm, - color: AppColors.textSecondary, - }, - section: { - marginTop: Spacing.md, - }, subscriptionCard: { - backgroundColor: AppColors.background, - marginHorizontal: Spacing.lg, - borderRadius: BorderRadius.lg, - overflow: 'hidden', + backgroundColor: AppColors.surface, + borderRadius: BorderRadius.xl, + padding: Spacing.lg, borderWidth: 2, borderColor: AppColors.primary, + width: '100%', + ...Shadows.md, }, cardHeader: { - backgroundColor: `${AppColors.primary}10`, - padding: Spacing.lg, alignItems: 'center', }, - proBadge: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.xs, - }, proBadgeText: { fontSize: FontSizes.lg, fontWeight: FontWeights.bold, - color: AppColors.primary, + color: AppColors.textPrimary, }, priceContainer: { flexDirection: 'row', alignItems: 'baseline', - marginTop: Spacing.md, + marginTop: Spacing.sm, }, priceAmount: { - fontSize: 48, + fontSize: FontSizes['3xl'], fontWeight: FontWeights.bold, - color: AppColors.textPrimary, + color: AppColors.primary, }, pricePeriod: { fontSize: FontSizes.lg, color: AppColors.textSecondary, marginLeft: Spacing.xs, }, - featuresContainer: { - padding: Spacing.lg, + planDescription: { + fontSize: FontSizes.base, + color: AppColors.textSecondary, + textAlign: 'center', + lineHeight: 22, + marginTop: Spacing.md, }, - featureRow: { + activeBadge: { flexDirection: 'row', alignItems: 'center', - paddingVertical: Spacing.xs, + backgroundColor: '#D1FAE5', + paddingHorizontal: Spacing.md, + paddingVertical: Spacing.sm, + borderRadius: BorderRadius.lg, + gap: Spacing.xs, + marginTop: Spacing.md, }, - featureText: { + activeBadgeText: { fontSize: FontSizes.sm, - color: AppColors.textPrimary, - marginLeft: Spacing.sm, + fontWeight: FontWeights.medium, + color: AppColors.success, }, - featureTextDisabled: { - color: AppColors.textMuted, + 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', @@ -784,9 +681,7 @@ const styles = StyleSheet.create({ justifyContent: 'center', gap: Spacing.sm, backgroundColor: AppColors.primary, - marginHorizontal: Spacing.lg, - marginBottom: Spacing.lg, - paddingVertical: Spacing.lg, + paddingVertical: Spacing.md, borderRadius: BorderRadius.lg, }, buttonDisabled: { @@ -803,52 +698,63 @@ const styles = StyleSheet.create({ justifyContent: 'center', gap: Spacing.sm, backgroundColor: AppColors.success, - marginHorizontal: Spacing.lg, - marginBottom: Spacing.lg, - paddingVertical: Spacing.lg, + paddingVertical: Spacing.md, borderRadius: BorderRadius.lg, }, - securityBadge: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: Spacing.xs, - marginTop: Spacing.lg, - paddingVertical: Spacing.md, - }, - securityText: { - fontSize: FontSizes.sm, - color: AppColors.success, - }, - linksSection: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: Spacing.md, - gap: Spacing.sm, - }, linkButton: { paddingVertical: Spacing.xs, - paddingHorizontal: Spacing.xs, - }, - linkButtonText: { - fontSize: FontSizes.sm, - color: AppColors.primary, + alignItems: 'center', }, linkButtonTextMuted: { fontSize: FontSizes.sm, color: AppColors.textMuted, + textDecorationLine: 'underline', }, - linkDivider: { - fontSize: FontSizes.sm, - color: AppColors.textMuted, + // Modal styles + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + padding: Spacing.lg, }, - termsText: { - fontSize: FontSizes.xs, - color: AppColors.textMuted, + 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, - paddingBottom: Spacing.xl, - lineHeight: 16, + borderRadius: BorderRadius.lg, + width: '100%', + }, + modalButtonText: { + fontSize: FontSizes.base, + fontWeight: FontWeights.semibold, + color: AppColors.white, + textAlign: 'center', }, }); diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index d14d503..4df66ca 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -232,7 +232,11 @@ export default function HomeScreen() { }; const getDisplayName = () => { - if (user?.user_name) return user.user_name; + // Check firstName/lastName from API + if (user?.firstName) { + return user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName; + } + // Fallback to email prefix if (user?.email) return user.email.split('@')[0]; return 'User'; }; diff --git a/app/(tabs)/profile/edit.tsx b/app/(tabs)/profile/edit.tsx index 985810c..bc353db 100644 --- a/app/(tabs)/profile/edit.tsx +++ b/app/(tabs)/profile/edit.tsx @@ -89,7 +89,6 @@ export default function EditProfileScreen() { // Update user in AuthContext (will refetch from API) if (updateUser) { updateUser({ - user_name: displayName.trim(), firstName, lastName, phone, diff --git a/app/(tabs)/profile/index.tsx b/app/(tabs)/profile/index.tsx index ed63155..7bb7835 100644 --- a/app/(tabs)/profile/index.tsx +++ b/app/(tabs)/profile/index.tsx @@ -120,8 +120,14 @@ export default function ProfileScreen() { ); }; - const userName = user?.user_name || 'User'; - const userInitial = userName.charAt(0).toUpperCase(); + const displayName = useMemo(() => { + if (user?.firstName) { + return user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName; + } + if (user?.email) return user.email.split('@')[0]; + return 'User'; + }, [user?.firstName, user?.lastName, user?.email]); + const userInitial = displayName.charAt(0).toUpperCase(); // Generate invite code based on user email or id const inviteCode = useMemo(() => { @@ -168,7 +174,7 @@ export default function ProfileScreen() { - {userName} + {displayName} {user?.email || ''} {/* Invite Code */} @@ -326,7 +332,7 @@ const styles = StyleSheet.create({ borderWidth: 3, borderColor: AppColors.surface, }, - userName: { + displayName: { fontSize: FontSizes['2xl'], fontWeight: FontWeights.bold, color: AppColors.textPrimary, diff --git a/app/(tabs)/voice.tsx b/app/(tabs)/voice.tsx index b3d3508..437a43c 100644 --- a/app/(tabs)/voice.tsx +++ b/app/(tabs)/voice.tsx @@ -254,14 +254,13 @@ export default function VoiceAIScreen() { }, [isSpeaking]); // Fetch activity data - const getActivityContext = async (token: string, userName: string, deploymentId: string): Promise => { + const getActivityContext = async (token: string, deploymentId: string): Promise => { try { const response = await fetch(OLD_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ function: 'activities_report_details', - user_name: userName, token: token, deployment_id: deploymentId, filter: '0', @@ -298,7 +297,7 @@ export default function VoiceAIScreen() { } }; - const getDashboardContext = async (token: string, userName: string, deploymentId: string): Promise => { + const getDashboardContext = async (token: string, deploymentId: string): Promise => { try { const today = new Date().toISOString().split('T')[0]; const response = await fetch(OLD_API_URL, { @@ -306,7 +305,6 @@ export default function VoiceAIScreen() { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ function: 'dashboard_single', - user_name: userName, token: token, deployment_id: deploymentId, date: today, @@ -331,17 +329,16 @@ export default function VoiceAIScreen() { const sendToVoiceAsk = async (question: string): Promise => { const token = await SecureStore.getItemAsync('accessToken'); - const userName = await SecureStore.getItemAsync('userName'); - if (!token || !userName) throw new Error('Please log in to use voice assistant'); + if (!token) throw new Error('Please log in to use voice assistant'); if (!currentBeneficiary?.id) throw new Error('Please select a beneficiary first'); const beneficiaryName = currentBeneficiary.name || 'the patient'; const deploymentId = currentBeneficiary.id.toString(); - let activityContext = await getActivityContext(token, userName, deploymentId); + let activityContext = await getActivityContext(token, deploymentId); if (!activityContext) { - activityContext = await getDashboardContext(token, userName, deploymentId); + activityContext = await getDashboardContext(token, deploymentId); } let enhancedQuestion: string; @@ -357,7 +354,6 @@ export default function VoiceAIScreen() { body: new URLSearchParams({ function: 'voice_ask', clientId: '001', - user_name: userName, token: token, question: enhancedQuestion, deployment_id: deploymentId, diff --git a/backend/src/config/stripe.js b/backend/src/config/stripe.js index a3701cb..791e0e0 100644 --- a/backend/src/config/stripe.js +++ b/backend/src/config/stripe.js @@ -15,7 +15,7 @@ const PRODUCTS = { PREMIUM_SUBSCRIPTION: { name: 'WellNuo Premium', description: 'AI Julia chat, 90-day history, invite up to 5 family members', - price: 999, // $9.99 in cents + price: 4900, // $49.00 in cents type: 'recurring', interval: 'month' } diff --git a/backend/src/routes/stripe.js b/backend/src/routes/stripe.js index cd97356..ba10b19 100644 --- a/backend/src/routes/stripe.js +++ b/backend/src/routes/stripe.js @@ -209,10 +209,10 @@ router.get('/products', async (req, res) => { * Customer is tied to BENEFICIARY (not user) so subscription persists when access is transferred */ async function getOrCreateStripeCustomer(beneficiaryId) { - // Get beneficiary from DB + // Get beneficiary from DB (new beneficiaries table) const { data: beneficiary, error } = await supabase - .from('users') - .select('id, email, first_name, last_name, stripe_customer_id') + .from('beneficiaries') + .select('id, name, stripe_customer_id') .eq('id', beneficiaryId) .single(); @@ -227,8 +227,7 @@ async function getOrCreateStripeCustomer(beneficiaryId) { // Create new Stripe customer for this beneficiary const customer = await stripe.customers.create({ - email: beneficiary.email, - name: `${beneficiary.first_name || ''} ${beneficiary.last_name || ''}`.trim() || undefined, + name: beneficiary.name || undefined, metadata: { beneficiary_id: beneficiary.id.toString(), type: 'beneficiary' @@ -237,7 +236,7 @@ async function getOrCreateStripeCustomer(beneficiaryId) { // Save stripe_customer_id to DB await supabase - .from('users') + .from('beneficiaries') .update({ stripe_customer_id: customer.id }) .eq('id', beneficiaryId); @@ -354,7 +353,7 @@ router.get('/subscription-status/:beneficiaryId', async (req, res) => { // Get beneficiary's stripe_customer_id const { data: beneficiary } = await supabase - .from('users') + .from('beneficiaries') .select('stripe_customer_id') .eq('id', beneficiaryId) .single(); @@ -432,7 +431,7 @@ router.post('/cancel-subscription', async (req, res) => { // Get beneficiary's stripe_customer_id const { data: beneficiary } = await supabase - .from('users') + .from('beneficiaries') .select('stripe_customer_id') .eq('id', beneficiaryId) .single(); @@ -484,7 +483,7 @@ router.post('/reactivate-subscription', async (req, res) => { } const { data: beneficiary } = await supabase - .from('users') + .from('beneficiaries') .select('stripe_customer_id') .eq('id', beneficiaryId) .single(); @@ -591,6 +590,42 @@ router.post('/create-subscription-payment-sheet', async (req, res) => { } }); +/** + * POST /api/stripe/confirm-subscription-payment + * Confirms the latest invoice PaymentIntent for a subscription if needed + */ +router.post('/confirm-subscription-payment', async (req, res) => { + try { + const { subscriptionId } = req.body; + + if (!subscriptionId) { + return res.status(400).json({ error: 'subscriptionId is required' }); + } + + const subscription = await stripe.subscriptions.retrieve(subscriptionId, { + expand: ['latest_invoice.payment_intent'] + }); + + const paymentIntent = subscription.latest_invoice?.payment_intent; + const paymentIntentId = typeof paymentIntent === 'string' ? paymentIntent : paymentIntent?.id; + const paymentIntentStatus = typeof paymentIntent === 'string' ? null : paymentIntent?.status; + + if (!paymentIntentId) { + return res.status(400).json({ error: 'Payment intent not found for subscription' }); + } + + if (paymentIntentStatus === 'requires_confirmation') { + const confirmed = await stripe.paymentIntents.confirm(paymentIntentId); + return res.json({ success: true, status: confirmed.status }); + } + + return res.json({ success: true, status: paymentIntentStatus || 'unknown' }); + } catch (error) { + console.error('Confirm subscription payment error:', error); + res.status(500).json({ error: error.message }); + } +}); + /** * GET /api/stripe/session/:sessionId * Get checkout session details (for success page) diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx index 04cb519..4b4364f 100644 --- a/contexts/AuthContext.tsx +++ b/contexts/AuthContext.tsx @@ -2,9 +2,6 @@ import { api, setOnUnauthorizedCallback } from '@/services/api'; import type { ApiError, User } from '@/types'; import React, { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react'; -// Test account for development - uses legacy anandk credentials -const DEV_EMAIL = 'serter2069@gmail.com'; - interface AuthState { user: User | null; isLoading: boolean; @@ -13,7 +10,24 @@ interface AuthState { error: ApiError | null; } -// ... +type CheckEmailResult = { exists: boolean; name?: string | null }; +type OtpResult = { success: boolean; skipOtp: boolean }; +type UserProfileUpdate = Partial & { + firstName?: string | null; + lastName?: string | null; + phone?: string | null; + email?: string; +}; + +interface AuthContextType extends AuthState { + checkEmail: (email: string) => Promise; + requestOtp: (email: string) => Promise; + verifyOtp: (email: string, code: string) => Promise; + logout: () => Promise; + clearError: () => void; + refreshAuth: () => Promise; + updateUser: (updates: UserProfileUpdate) => void; +} const AuthContext = createContext(null); @@ -30,7 +44,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { useEffect(() => { console.log('[AuthContext] checkAuth starting...'); checkAuth(); - }, []); + }, [checkAuth]); // Auto-logout when WellNuo API returns 401 (token expired) // Token now expires after 365 days, so this should rarely happen @@ -49,7 +63,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { }); }, []); - const checkAuth = async () => { + const checkAuth = useCallback(async () => { try { console.log(`[AuthContext] checkAuth: Checking token...`); const token = await api.getToken(); @@ -91,7 +105,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { } finally { console.log(`[AuthContext] checkAuth: Finished`); } - }; + }, []); const checkEmail = useCallback(async (email: string): Promise => { setState((prev) => ({ ...prev, isLoading: true, error: null })); @@ -157,23 +171,15 @@ export function AuthProvider({ children }: { children: ReactNode }) { setState((prev) => ({ ...prev, isLoading: true, error: null })); try { - // Dev account - also login to legacy API for dashboard access - if (email.toLowerCase() === DEV_EMAIL.toLowerCase()) { - // Login with legacy API to get dashboard access - const legacyResponse = await api.login('anandk', 'anandk_8'); - if (legacyResponse.ok && legacyResponse.data) { - console.log('[AuthContext] Dev mode: Legacy API login successful'); - } - } - // Verify OTP via WellNuo API (for all users including dev) const verifyResponse = await api.verifyOTP(email, code); if (verifyResponse.ok && verifyResponse.data) { const user: User = { user_id: verifyResponse.data.user.id, - user_name: verifyResponse.data.user.first_name || email.split('@')[0], email: email, + firstName: verifyResponse.data.user.first_name || null, + lastName: verifyResponse.data.user.last_name || null, max_role: 'USER', privileges: '', }; @@ -205,6 +211,17 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }, []); + const refreshAuth = useCallback(async () => { + await checkAuth(); + }, [checkAuth]); + + const updateUser = useCallback((updates: UserProfileUpdate) => { + setState((prev) => { + if (!prev.user) return prev; + return { ...prev, user: { ...prev.user, ...updates } }; + }); + }, []); + const logout = useCallback(async () => { setState((prev) => ({ ...prev, isLoading: true })); @@ -225,7 +242,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { }, []); return ( - + {children} ); diff --git a/services/api.ts b/services/api.ts index 2128d34..dd957d2 100644 --- a/services/api.ts +++ b/services/api.ts @@ -55,14 +55,6 @@ class ApiService { } } - private async getUserName(): Promise { - try { - return await SecureStore.getItemAsync('userName'); - } catch { - return null; - } - } - // Get legacy API token (for eluxnetworks.net API - dashboard, voice_ask) private async getLegacyToken(): Promise { try { @@ -72,20 +64,6 @@ class ApiService { } } - // Save legacy API credentials (for dev mode with anandk account) - async saveLegacyCredentials(token: string, userName: string): Promise { - await SecureStore.setItemAsync('legacyAccessToken', token); - await SecureStore.setItemAsync('legacyUserName', userName); - } - - private async getLegacyUserName(): Promise { - try { - return await SecureStore.getItemAsync('legacyUserName'); - } catch { - return null; - } - } - private generateNonce(): string { const randomBytes = Crypto.getRandomBytes(16); return Array.from(randomBytes) @@ -150,7 +128,7 @@ class ApiService { async login(username: string, password: string): Promise> { const response = await this.makeRequest({ function: 'credentials', - user_name: username, + email: username, ps: password, clientId: CLIENT_ID, nonce: this.generateNonce(), @@ -160,10 +138,8 @@ class ApiService { // Save LEGACY credentials separately (not to accessToken!) // accessToken is reserved for WellNuo API JWT tokens await SecureStore.setItemAsync('legacyAccessToken', response.data.access_token); - await SecureStore.setItemAsync('legacyUserName', username); // Keep these for backward compatibility await SecureStore.setItemAsync('userId', response.data.user_id.toString()); - await SecureStore.setItemAsync('userName', username); await SecureStore.setItemAsync('privileges', response.data.privileges); await SecureStore.setItemAsync('maxRole', response.data.max_role.toString()); } @@ -179,9 +155,6 @@ class ApiService { await SecureStore.deleteItemAsync('onboardingCompleted'); // Clear legacy API auth data await SecureStore.deleteItemAsync('legacyAccessToken'); - await SecureStore.deleteItemAsync('legacyUserName'); - // Legacy cleanup (can be removed later) - await SecureStore.deleteItemAsync('userName'); await SecureStore.deleteItemAsync('privileges'); await SecureStore.deleteItemAsync('maxRole'); } @@ -215,10 +188,9 @@ class ApiService { } // Save mock user (for dev mode OTP flow) - async saveMockUser(user: { user_id: string; user_name: string; email: string; max_role: string; privileges: string[] }): Promise { + async saveMockUser(user: { user_id: string; email: string; max_role: string; privileges: string[] }): Promise { await SecureStore.setItemAsync('accessToken', `mock-token-${user.user_id}`); await SecureStore.setItemAsync('userId', user.user_id); - await SecureStore.setItemAsync('userName', user.user_name); await SecureStore.setItemAsync('privileges', user.privileges.join(',')); await SecureStore.setItemAsync('maxRole', user.max_role); await SecureStore.setItemAsync('userEmail', user.email); @@ -362,7 +334,6 @@ class ApiService { const email = await SecureStore.getItemAsync('userEmail'); return { user_id: parseInt(userId, 10), - user_name: email?.split('@')[0] || 'User', email: email || undefined, privileges: '', max_role: 0, @@ -375,7 +346,6 @@ class ApiService { return { user_id: userData.id, - user_name: userData.firstName || userData.email?.split('@')[0] || 'User', email: userData.email, firstName: userData.firstName, lastName: userData.lastName, @@ -391,7 +361,6 @@ class ApiService { if (userId) { return { user_id: parseInt(userId, 10), - user_name: email?.split('@')[0] || 'User', email: email || undefined, privileges: '', max_role: 0, @@ -575,13 +544,11 @@ class ApiService { async getBeneficiaryDashboard(deploymentId: string): Promise> { // Use legacy API credentials for dashboard const token = await this.getLegacyToken(); - const userName = await this.getLegacyUserName(); - if (!token || !userName) { + if (!token) { // Fallback to regular credentials if legacy not available const fallbackToken = await this.getToken(); - const fallbackUserName = await this.getUserName(); - if (!fallbackToken || !fallbackUserName) { + if (!fallbackToken) { return { ok: false, error: { message: 'Not authenticated for dashboard access', code: 'UNAUTHORIZED' } }; } // Note: This will likely fail if using WellNuo token, but we try anyway @@ -591,7 +558,6 @@ class ApiService { const response = await this.makeRequest({ function: 'dashboard_single', - user_name: userName || await this.getUserName() || '', token: token || await this.getToken() || '', deployment_id: deploymentId, date: today, @@ -863,16 +829,14 @@ class ApiService { } // Use legacy API credentials for voice_ask const token = await this.getLegacyToken() || await this.getToken(); - const userName = await this.getLegacyUserName() || await this.getUserName(); - if (!token || !userName) { + if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } return this.makeRequest({ function: 'voice_ask', clientId: CLIENT_ID, - user_name: userName, token: token, question: question, deployment_id: deploymentId, diff --git a/types/index.ts b/types/index.ts index 5e58587..c595350 100644 --- a/types/index.ts +++ b/types/index.ts @@ -2,8 +2,10 @@ // User & Auth Types export interface User { user_id: number | string; - user_name: string; email?: string; + firstName?: string | null; + lastName?: string | null; + phone?: string | null; max_role: number | string; privileges: string | string[]; }