diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx
index 9ac3ffe..2a9a3fc 100644
--- a/app/(auth)/_layout.tsx
+++ b/app/(auth)/_layout.tsx
@@ -10,6 +10,10 @@ export default function AuthLayout() {
}}
>
+
+
+
+
);
}
diff --git a/app/(auth)/activate.tsx b/app/(auth)/activate.tsx
new file mode 100644
index 0000000..bd62cf4
--- /dev/null
+++ b/app/(auth)/activate.tsx
@@ -0,0 +1,479 @@
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ TextInput,
+ ScrollView,
+ ActivityIndicator,
+ Alert,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { router } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { AppColors, Spacing, BorderRadius, FontSizes, FontWeights } from '@/constants/theme';
+
+// DEV MODE: Mock activation code
+const MOCK_ACTIVATION_CODE = 'WELLNUO-DEMO';
+
+export default function ActivateScreen() {
+ const [activationCode, setActivationCode] = useState('');
+ const [isActivating, setIsActivating] = useState(false);
+ const [step, setStep] = useState<'code' | 'beneficiary' | 'complete'>('code');
+ const [beneficiaryName, setBeneficiaryName] = useState('');
+ const [relationship, setRelationship] = useState('');
+
+ const handleActivate = async () => {
+ if (!activationCode.trim()) {
+ Alert.alert('Error', 'Please enter activation code');
+ return;
+ }
+
+ setIsActivating(true);
+
+ // DEV MODE: Simulate activation
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+
+ // Accept any code in dev mode, or use the mock code
+ if (activationCode.toUpperCase() === MOCK_ACTIVATION_CODE || activationCode.length >= 6) {
+ setStep('beneficiary');
+ } else {
+ Alert.alert('Invalid Code', 'Please check your activation code and try again.\n\nDEV: Use "WELLNUO-DEMO" or any 6+ character code.');
+ }
+
+ setIsActivating(false);
+ };
+
+ const handleAddBeneficiary = async () => {
+ if (!beneficiaryName.trim()) {
+ Alert.alert('Error', 'Please enter beneficiary name');
+ return;
+ }
+
+ setIsActivating(true);
+
+ // DEV MODE: Simulate adding beneficiary
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ setStep('complete');
+ setIsActivating(false);
+ };
+
+ const handleComplete = () => {
+ // Navigate to main app
+ router.replace('/(tabs)');
+ };
+
+ const handleUseDemoCode = () => {
+ setActivationCode(MOCK_ACTIVATION_CODE);
+ };
+
+ // Step 1: Enter activation code
+ if (step === 'code') {
+ return (
+
+
+ {/* Header */}
+
+ router.back()}>
+
+
+ Activate Kit
+
+
+
+ {/* Icon */}
+
+
+
+
+ {/* Instructions */}
+
+ Enter the activation code from your WellNuo Starter Kit packaging
+
+
+ {/* Input */}
+
+
+
+
+ {/* Development Notice */}
+
+
+ Sensor Activation In Development
+
+ Real sensor pairing is coming soon. For now, enter any code (6+ characters) to continue testing the app.
+
+
+ Use Demo Code
+
+
+
+ {/* Activate Button */}
+
+ {isActivating ? (
+
+ ) : (
+ Activate
+ )}
+
+
+ {/* Skip for now */}
+
+ Skip for now
+
+
+
+ );
+ }
+
+ // Step 2: Add beneficiary
+ if (step === 'beneficiary') {
+ return (
+
+
+ {/* Header */}
+
+ setStep('code')}>
+
+
+ Add Beneficiary
+
+
+
+ {/* Icon */}
+
+
+
+
+ {/* Instructions */}
+
+ Who will you be monitoring with this kit?
+
+
+ {/* Name Input */}
+
+ Full Name
+
+
+
+ {/* Relationship Input */}
+
+ Relationship (optional)
+
+
+
+ {/* Relationship Quick Select */}
+
+ {['Mother', 'Father', 'Grandmother', 'Grandfather'].map((rel) => (
+ setRelationship(rel)}
+ >
+
+ {rel}
+
+
+ ))}
+
+
+ {/* Continue Button */}
+
+ {isActivating ? (
+
+ ) : (
+ Continue
+ )}
+
+
+
+ );
+ }
+
+ // Step 3: Complete
+ return (
+
+
+ {/* Success Icon */}
+
+
+
+
+
+ Kit Activated!
+
+ Your WellNuo kit has been successfully activated for{' '}
+ {beneficiaryName}
+
+
+ {/* Next Steps */}
+
+ Next Steps:
+
+ 1
+ Place sensors in your loved one's home
+
+
+ 2
+ Connect the hub to WiFi
+
+
+ 3
+ Start receiving activity updates
+
+
+
+
+ {/* Complete Button */}
+
+ Go to Dashboard
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: AppColors.background,
+ },
+ content: {
+ flex: 1,
+ padding: Spacing.lg,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginBottom: Spacing.xl,
+ },
+ backButton: {
+ padding: Spacing.sm,
+ marginLeft: -Spacing.sm,
+ },
+ title: {
+ fontSize: FontSizes.xl,
+ fontWeight: FontWeights.bold,
+ color: AppColors.textPrimary,
+ },
+ placeholder: {
+ width: 40,
+ },
+ iconContainer: {
+ alignItems: 'center',
+ marginBottom: Spacing.xl,
+ },
+ instructions: {
+ fontSize: FontSizes.base,
+ color: AppColors.textSecondary,
+ textAlign: 'center',
+ marginBottom: Spacing.xl,
+ lineHeight: 24,
+ },
+ inputContainer: {
+ marginBottom: Spacing.lg,
+ },
+ inputGroup: {
+ marginBottom: Spacing.lg,
+ },
+ inputLabel: {
+ fontSize: FontSizes.sm,
+ fontWeight: FontWeights.medium,
+ color: AppColors.textPrimary,
+ marginBottom: Spacing.sm,
+ },
+ input: {
+ backgroundColor: AppColors.surface,
+ borderRadius: BorderRadius.lg,
+ paddingHorizontal: Spacing.lg,
+ paddingVertical: Spacing.md,
+ fontSize: FontSizes.lg,
+ color: AppColors.textPrimary,
+ textAlign: 'center',
+ borderWidth: 1,
+ borderColor: AppColors.border,
+ },
+ devNotice: {
+ alignItems: 'center',
+ marginBottom: Spacing.xl,
+ paddingVertical: Spacing.lg,
+ paddingHorizontal: Spacing.lg,
+ backgroundColor: `${AppColors.warning}10`,
+ borderRadius: BorderRadius.lg,
+ borderWidth: 1,
+ borderColor: `${AppColors.warning}30`,
+ },
+ devNoticeTitle: {
+ fontSize: FontSizes.base,
+ fontWeight: FontWeights.semibold,
+ color: AppColors.warning,
+ marginTop: Spacing.sm,
+ marginBottom: Spacing.sm,
+ },
+ devNoticeText: {
+ fontSize: FontSizes.sm,
+ color: AppColors.textSecondary,
+ textAlign: 'center',
+ lineHeight: 20,
+ marginBottom: Spacing.md,
+ },
+ demoCodeButton: {
+ paddingVertical: Spacing.sm,
+ paddingHorizontal: Spacing.lg,
+ backgroundColor: AppColors.warning,
+ borderRadius: BorderRadius.md,
+ },
+ demoCodeButtonText: {
+ fontSize: FontSizes.sm,
+ fontWeight: FontWeights.medium,
+ color: AppColors.white,
+ },
+ primaryButton: {
+ backgroundColor: AppColors.primary,
+ paddingVertical: Spacing.lg,
+ borderRadius: BorderRadius.lg,
+ alignItems: 'center',
+ marginTop: Spacing.md,
+ },
+ buttonDisabled: {
+ opacity: 0.7,
+ },
+ primaryButtonText: {
+ fontSize: FontSizes.lg,
+ fontWeight: FontWeights.semibold,
+ color: AppColors.white,
+ },
+ skipButton: {
+ alignItems: 'center',
+ paddingVertical: Spacing.lg,
+ },
+ skipButtonText: {
+ fontSize: FontSizes.base,
+ color: AppColors.textSecondary,
+ textDecorationLine: 'underline',
+ },
+ quickSelect: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: Spacing.sm,
+ marginBottom: Spacing.xl,
+ },
+ quickSelectButton: {
+ paddingHorizontal: Spacing.md,
+ paddingVertical: Spacing.sm,
+ borderRadius: BorderRadius.full,
+ backgroundColor: AppColors.surface,
+ borderWidth: 1,
+ borderColor: AppColors.border,
+ },
+ quickSelectButtonActive: {
+ backgroundColor: AppColors.primary,
+ borderColor: AppColors.primary,
+ },
+ quickSelectText: {
+ fontSize: FontSizes.sm,
+ color: AppColors.textSecondary,
+ },
+ quickSelectTextActive: {
+ color: AppColors.white,
+ fontWeight: FontWeights.medium,
+ },
+ successContainer: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ successIcon: {
+ marginBottom: Spacing.xl,
+ },
+ successTitle: {
+ fontSize: FontSizes['2xl'],
+ fontWeight: FontWeights.bold,
+ color: AppColors.textPrimary,
+ marginBottom: Spacing.md,
+ },
+ successMessage: {
+ fontSize: FontSizes.base,
+ color: AppColors.textSecondary,
+ textAlign: 'center',
+ marginBottom: Spacing.xxl,
+ },
+ beneficiaryHighlight: {
+ fontWeight: FontWeights.bold,
+ color: AppColors.primary,
+ },
+ nextSteps: {
+ width: '100%',
+ backgroundColor: AppColors.surface,
+ borderRadius: BorderRadius.lg,
+ padding: Spacing.lg,
+ },
+ nextStepsTitle: {
+ fontSize: FontSizes.base,
+ fontWeight: FontWeights.semibold,
+ color: AppColors.textPrimary,
+ marginBottom: Spacing.md,
+ },
+ stepItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: Spacing.md,
+ marginBottom: Spacing.sm,
+ },
+ stepNumber: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ backgroundColor: AppColors.primary,
+ color: AppColors.white,
+ textAlign: 'center',
+ lineHeight: 24,
+ fontSize: FontSizes.sm,
+ fontWeight: FontWeights.bold,
+ overflow: 'hidden',
+ },
+ stepText: {
+ fontSize: FontSizes.sm,
+ color: AppColors.textSecondary,
+ flex: 1,
+ },
+});
diff --git a/app/(auth)/purchase.tsx b/app/(auth)/purchase.tsx
new file mode 100644
index 0000000..9d23ed4
--- /dev/null
+++ b/app/(auth)/purchase.tsx
@@ -0,0 +1,312 @@
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ ScrollView,
+ ActivityIndicator,
+ Alert,
+ Linking,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { router } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { AppColors, Spacing, BorderRadius, FontSizes, FontWeights } from '@/constants/theme';
+import { useAuth } from '@/contexts/AuthContext';
+
+// Stripe Checkout API
+const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe';
+
+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() {
+ const [isProcessing, setIsProcessing] = useState(false);
+ const { user } = useAuth();
+
+ const handlePurchase = async () => {
+ setIsProcessing(true);
+
+ try {
+ // Create Stripe Checkout session via our API
+ const response = await fetch(`${STRIPE_API_URL}/create-checkout-session`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ userId: user?.user_id || 'guest',
+ email: user?.email,
+ beneficiaryName: 'To be configured',
+ beneficiaryAddress: 'To be configured',
+ includePremium: false, // Just starter kit for now
+ }),
+ });
+
+ const data = await response.json();
+
+ if (data.url) {
+ // Open Stripe Checkout in browser
+ const canOpen = await Linking.canOpenURL(data.url);
+ if (canOpen) {
+ await Linking.openURL(data.url);
+ // After returning from browser, go to activate
+ Alert.alert(
+ 'Complete Your Purchase',
+ 'After completing payment in the browser, tap "Continue" to activate your kit.',
+ [
+ {
+ text: 'Continue',
+ onPress: () => router.replace('/(auth)/activate'),
+ },
+ ]
+ );
+ } else {
+ Alert.alert('Error', 'Could not open payment page');
+ }
+ } else {
+ Alert.alert('Error', data.error || 'Failed to create checkout session');
+ }
+ } catch (error) {
+ console.error('Stripe checkout error:', error);
+ Alert.alert('Error', 'Failed to connect to payment service. Please try again.');
+ }
+
+ setIsProcessing(false);
+ };
+
+ const handleSkip = () => {
+ // For users who already have a kit
+ router.replace('/(auth)/activate');
+ };
+
+ return (
+
+
+ {/* Header */}
+
+ router.back()}>
+
+
+ Get Started
+
+
+
+ {/* Product Card */}
+
+
+
+
+ {STARTER_KIT.name}
+ {STARTER_KIT.price}
+ {STARTER_KIT.description}
+
+ {/* Features */}
+
+ {STARTER_KIT.features.map((feature, index) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+ {/* Security Badge */}
+
+
+
+ Secure payment powered by Stripe
+
+
+
+ {/* Test Mode Notice */}
+
+
+
+ Test mode: Use card 4242 4242 4242 4242
+
+
+
+
+ {/* Bottom Actions */}
+
+
+ {isProcessing ? (
+
+ ) : (
+ <>
+
+ Buy Now - {STARTER_KIT.price}
+ >
+ )}
+
+
+
+ I already have a kit
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: AppColors.background,
+ },
+ content: {
+ padding: Spacing.lg,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginBottom: Spacing.xl,
+ },
+ backButton: {
+ padding: Spacing.sm,
+ marginLeft: -Spacing.sm,
+ },
+ title: {
+ fontSize: FontSizes.xl,
+ fontWeight: FontWeights.bold,
+ color: AppColors.textPrimary,
+ },
+ placeholder: {
+ width: 40,
+ },
+ productCard: {
+ backgroundColor: AppColors.white,
+ borderRadius: BorderRadius.xl,
+ padding: Spacing.xl,
+ alignItems: 'center',
+ borderWidth: 2,
+ borderColor: AppColors.primary,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.1,
+ shadowRadius: 12,
+ elevation: 5,
+ },
+ productIcon: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ backgroundColor: `${AppColors.primary}15`,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: Spacing.lg,
+ },
+ productName: {
+ fontSize: FontSizes['2xl'],
+ fontWeight: FontWeights.bold,
+ color: AppColors.textPrimary,
+ textAlign: 'center',
+ marginBottom: Spacing.sm,
+ },
+ productPrice: {
+ fontSize: FontSizes['3xl'],
+ fontWeight: FontWeights.bold,
+ color: AppColors.primary,
+ marginBottom: Spacing.sm,
+ },
+ 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,
+ },
+ securityBadge: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: Spacing.sm,
+ marginTop: Spacing.xl,
+ paddingVertical: Spacing.md,
+ backgroundColor: `${AppColors.success}10`,
+ borderRadius: BorderRadius.lg,
+ },
+ securityText: {
+ fontSize: FontSizes.sm,
+ color: AppColors.success,
+ },
+ testNotice: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: Spacing.sm,
+ marginTop: Spacing.md,
+ paddingVertical: Spacing.sm,
+ paddingHorizontal: Spacing.md,
+ backgroundColor: `${AppColors.primary}10`,
+ borderRadius: BorderRadius.md,
+ },
+ testNoticeText: {
+ fontSize: FontSizes.xs,
+ color: AppColors.primary,
+ fontWeight: FontWeights.medium,
+ },
+ bottomActions: {
+ padding: Spacing.lg,
+ paddingBottom: Spacing.xl,
+ gap: Spacing.md,
+ },
+ purchaseButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: Spacing.sm,
+ backgroundColor: AppColors.primary,
+ paddingVertical: Spacing.lg,
+ borderRadius: BorderRadius.lg,
+ },
+ buttonDisabled: {
+ opacity: 0.7,
+ },
+ purchaseButtonText: {
+ fontSize: FontSizes.lg,
+ fontWeight: FontWeights.semibold,
+ color: AppColors.white,
+ },
+ skipButton: {
+ alignItems: 'center',
+ paddingVertical: Spacing.md,
+ },
+ skipButtonText: {
+ fontSize: FontSizes.base,
+ color: AppColors.textSecondary,
+ textDecorationLine: 'underline',
+ },
+});
diff --git a/app/(auth)/verify-otp.tsx b/app/(auth)/verify-otp.tsx
index e008ba1..7b2591b 100644
--- a/app/(auth)/verify-otp.tsx
+++ b/app/(auth)/verify-otp.tsx
@@ -43,7 +43,10 @@ export default function VerifyOTPScreen() {
try {
const success = await verifyOtp(email!, '000000'); // Bypass code
if (success) {
- router.replace('/(tabs)');
+ // Dev account (serter2069@gmail.com) goes straight to dashboard (has data)
+ // Other accounts are new - send to purchase flow
+ const isDevEmail = email?.toLowerCase() === 'serter2069@gmail.com';
+ router.replace(isDevEmail ? '/(tabs)' : '/(auth)/purchase');
} else {
setError('Auto-login failed');
}
@@ -98,14 +101,20 @@ export default function VerifyOTPScreen() {
const success = await verifyOtp(email!, codeToVerify);
if (success) {
- router.replace('/(tabs)');
+ // Dev account (serter2069@gmail.com) goes straight to dashboard (has data)
+ // Other accounts are new - send to purchase flow
+ const isDevEmail = email?.toLowerCase() === 'serter2069@gmail.com';
+ router.replace(isDevEmail ? '/(tabs)' : '/(auth)/purchase');
} else {
- setError(authError?.message || 'Invalid code. Please try again.');
- setCode('');
+ // Show error but keep user on this screen to retry
+ setError('Invalid verification code. Please try again.');
+ setCode(''); // Clear code so user can re-enter
+ setTimeout(() => inputRef.current?.focus(), 100); // Re-focus input for retry
}
} catch (err) {
setError('Something went wrong. Please try again.');
setCode('');
+ setTimeout(() => inputRef.current?.focus(), 100);
} finally {
setIsLoading(false);
}
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index 1a14824..b4cfe46 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -78,6 +78,13 @@ export default function TabLayout() {
href: null,
}}
/>
+ {/* Hide voice tab - replaced by chat */}
+
);
}
diff --git a/backend/.env.example b/backend/.env.example
index e9e2965..5f7926d 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -13,7 +13,7 @@ NODE_ENV=production
# Brevo Email
BREVO_API_KEY=your-brevo-key
-BREVO_SENDER_EMAIL=noreply@wellnuo.com
+BREVO_SENDER_EMAIL=noreply@daterabbit.com
BREVO_SENDER_NAME=WellNuo
# Frontend URL (for password reset links)
diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx
index 11ceaf8..4d868df 100644
--- a/contexts/AuthContext.tsx
+++ b/contexts/AuthContext.tsx
@@ -2,7 +2,7 @@ import React, { createContext, useContext, useState, useEffect, useCallback, typ
import { api, setOnUnauthorizedCallback } from '@/services/api';
import type { User, ApiError } from '@/types';
-// Test account for development
+// Test account for development - uses legacy anandk credentials
const DEV_EMAIL = 'serter2069@gmail.com';
interface AuthState {
@@ -92,17 +92,28 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return { success: true, skipOtp: true };
}
- // For now, we'll just succeed - real OTP sending would happen via backend
- // In production, this would call: await api.requestOTP(email);
- setState((prev) => ({ ...prev, isLoading: false }));
- return { success: true, skipOtp: false };
+ // Send OTP via Brevo API
+ const response = await api.requestOTP(email);
+
+ if (response.ok) {
+ setState((prev) => ({ ...prev, isLoading: false }));
+ return { success: true, skipOtp: false };
+ }
+
+ // API failed
+ setState((prev) => ({
+ ...prev,
+ isLoading: false,
+ error: { message: 'Failed to send verification code. Please try again.' },
+ }));
+ return { success: false, skipOtp: false };
} catch (error) {
setState((prev) => ({
...prev,
isLoading: false,
- error: { message: error instanceof Error ? error.message : 'Failed to send OTP' },
+ error: { message: 'Network error. Please check your connection.' },
}));
- return { success: false };
+ return { success: false, skipOtp: false };
}
}, []);
@@ -145,13 +156,33 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return false;
}
- // For regular users - verify OTP via backend
- // In production: const response = await api.verifyOTP(email, code);
- // For now, fail as we don't have real OTP backend yet
+ // Verify OTP via API
+ 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,
+ max_role: 'USER',
+ privileges: [],
+ };
+
+ setState({
+ user,
+ isLoading: false,
+ isAuthenticated: true,
+ error: null,
+ });
+
+ return true;
+ }
+
+ // Wrong OTP code
setState((prev) => ({
...prev,
isLoading: false,
- error: { message: 'OTP verification not implemented yet. Use dev account.' },
+ error: { message: 'Invalid verification code. Please try again.' },
}));
return false;
} catch (error) {
diff --git a/services/api.ts b/services/api.ts
index 541f16d..6fa52d1 100644
--- a/services/api.ts
+++ b/services/api.ts
@@ -12,6 +12,10 @@ export function setOnUnauthorizedCallback(callback: () => void) {
const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api';
const CLIENT_ID = 'MA_001';
+// WellNuo Backend API (our own API for auth, OTP, etc.)
+// TODO: Update to production URL when deployed
+const WELLNUO_API_URL = 'https://wellnuo.smartlaunchhub.com/api';
+
// Avatar images for elderly beneficiaries - grandmothers (бабушки)
const ELDERLY_AVATARS = [
'https://images.unsplash.com/photo-1566616213894-2d4e1baee5d8?w=200&h=200&fit=crop&crop=face', // grandmother with gray hair
@@ -162,6 +166,84 @@ 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 {
+ 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);
+ }
+
+ // ==================== OTP Authentication (WellNuo Backend) ====================
+
+ // Request OTP code - sends email via Brevo
+ async requestOTP(email: string): Promise> {
+ try {
+ const response = await fetch(`${WELLNUO_API_URL}/auth/request-otp`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ return { data, ok: true };
+ }
+
+ return {
+ ok: false,
+ error: { message: data.error || 'Failed to send OTP' },
+ };
+ } catch (error) {
+ return {
+ ok: false,
+ error: { message: 'Network error. Please check your connection.' },
+ };
+ }
+ }
+
+ // Verify OTP code and get JWT token
+ async verifyOTP(email: string, code: string): Promise> {
+ try {
+ const response = await fetch(`${WELLNUO_API_URL}/auth/verify-otp`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email, code }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok && data.token) {
+ // Save auth data
+ await SecureStore.setItemAsync('accessToken', data.token);
+ await SecureStore.setItemAsync('userId', data.user.id);
+ await SecureStore.setItemAsync('userName', data.user.first_name || email.split('@')[0]);
+ await SecureStore.setItemAsync('userEmail', email);
+ await SecureStore.setItemAsync('maxRole', 'USER');
+ await SecureStore.setItemAsync('privileges', '');
+
+ return { data, ok: true };
+ }
+
+ return {
+ ok: false,
+ error: { message: data.error || 'Invalid or expired code' },
+ };
+ } catch (error) {
+ return {
+ ok: false,
+ error: { message: 'Network error. Please check your connection.' },
+ };
+ }
+ }
+
async isAuthenticated(): Promise {
const token = await this.getToken();
return !!token;