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[];
}