diff --git a/app/(auth)/verify-otp.tsx b/app/(auth)/verify-otp.tsx index 92e2dc0..e008ba1 100644 --- a/app/(auth)/verify-otp.tsx +++ b/app/(auth)/verify-otp.tsx @@ -8,11 +8,11 @@ import { ScrollView, TouchableOpacity, TextInput, + ActivityIndicator, } from 'react-native'; import { router, useLocalSearchParams } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useAuth } from '@/contexts/AuthContext'; -import { api } from '@/services/api'; import { Button } from '@/components/ui/Button'; import { ErrorMessage } from '@/components/ui/ErrorMessage'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; @@ -20,8 +20,8 @@ import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; const CODE_LENGTH = 6; export default function VerifyOTPScreen() { - const { email, isNewUser } = useLocalSearchParams<{ email: string; isNewUser: string }>(); - const { refreshAuth } = useAuth(); + const { email, skipOtp } = useLocalSearchParams<{ email: string; skipOtp: string }>(); + const { verifyOtp, requestOtp, isLoading: authLoading, error: authError, clearError } = useAuth(); const [code, setCode] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -31,12 +31,37 @@ export default function VerifyOTPScreen() { const inputRef = useRef(null); - // Focus input on mount + // Handle skip OTP (dev mode) useEffect(() => { - setTimeout(() => { - inputRef.current?.focus(); - }, 100); - }, []); + if (skipOtp === '1' && email) { + handleAutoLogin(); + } + }, [skipOtp, email]); + + const handleAutoLogin = async () => { + setIsLoading(true); + try { + const success = await verifyOtp(email!, '000000'); // Bypass code + if (success) { + router.replace('/(tabs)'); + } else { + setError('Auto-login failed'); + } + } catch (err) { + setError('Auto-login failed'); + } finally { + setIsLoading(false); + } + }; + + // Focus input on mount (only if not skipping OTP) + useEffect(() => { + if (skipOtp !== '1') { + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + } + }, [skipOtp]); // Countdown timer for resend useEffect(() => { @@ -70,26 +95,12 @@ export default function VerifyOTPScreen() { setError(null); try { - const response = await api.verifyOTP(email!, codeToVerify); + const success = await verifyOtp(email!, codeToVerify); - if (response.ok && response.data) { - // Refresh auth state - await refreshAuth(); - - // Check if new user needs to complete profile - const user = response.data.user; - if (!user.firstName || !user.lastName) { - // New user - go to complete profile - router.replace({ - pathname: '/(auth)/complete-profile', - params: { email: email! }, - }); - } else { - // Existing user - go to main app - router.replace('/(tabs)'); - } + if (success) { + router.replace('/(tabs)'); } else { - setError(response.error?.message || 'Invalid code. Please try again.'); + setError(authError?.message || 'Invalid code. Please try again.'); setCode(''); } } catch (err) { @@ -98,7 +109,7 @@ export default function VerifyOTPScreen() { } finally { setIsLoading(false); } - }, [code, email, refreshAuth]); + }, [code, email, verifyOtp, authError]); const handleResend = useCallback(async () => { if (resendCountdown > 0) return; @@ -107,20 +118,20 @@ export default function VerifyOTPScreen() { setError(null); try { - const response = await api.requestOTP(email!); + const result = await requestOtp(email!); - if (response.ok) { + if (result.success) { setResendCountdown(60); // 60 seconds cooldown setCode(''); } else { - setError(response.error?.message || 'Failed to resend code'); + setError('Failed to resend code'); } } catch (err) { setError('Failed to resend code. Please try again.'); } finally { setIsResending(false); } - }, [email, resendCountdown]); + }, [email, resendCountdown, requestOtp]); // Render code input boxes const renderCodeBoxes = () => { @@ -147,6 +158,16 @@ export default function VerifyOTPScreen() { return boxes; }; + // Show loading screen for auto-login + if (skipOtp === '1') { + return ( + + + Signing in... + + ); + } + return ( Promise; + requestOtp: (email: string) => Promise; + verifyOtp: (email: string, code: string) => Promise; logout: () => Promise; clearError: () => void; } @@ -73,44 +82,84 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }; - const login = useCallback(async (credentials: LoginCredentials): Promise => { + const requestOtp = useCallback(async (email: string): Promise => { setState((prev) => ({ ...prev, isLoading: true, error: null })); try { - const response = await api.login(credentials.username, credentials.password); - - if (response.ok && response.data) { - const user: User = { - user_id: response.data.user_id, - user_name: credentials.username, - max_role: response.data.max_role, - privileges: response.data.privileges, - }; - - setState({ - user, - isLoading: false, - isAuthenticated: true, - error: null, - }); - - return true; + // Check if dev email - skip OTP + if (email.toLowerCase() === DEV_EMAIL.toLowerCase()) { + setState((prev) => ({ ...prev, isLoading: false })); + 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 }; + } catch (error) { setState((prev) => ({ ...prev, isLoading: false, - error: response.error || { message: 'Login failed' }, + error: { message: error instanceof Error ? error.message : 'Failed to send OTP' }, })); + return { success: false }; + } + }, []); + const verifyOtp = useCallback(async (email: string, code: string): Promise => { + setState((prev) => ({ ...prev, isLoading: true, error: null })); + + try { + // Dev account bypass - use legacy credentials + if (email.toLowerCase() === DEV_EMAIL.toLowerCase()) { + // Login with legacy API using anandk credentials + const response = await api.login('anandk', 'anandk_8'); + + if (response.ok && response.data) { + const user: User = { + user_id: response.data.user_id, + user_name: 'anandk', + email: email, + max_role: response.data.max_role, + privileges: response.data.privileges, + }; + + // Save email to storage + await api.saveEmail(email); + + setState({ + user, + isLoading: false, + isAuthenticated: true, + error: null, + }); + + return true; + } + + setState((prev) => ({ + ...prev, + isLoading: false, + error: { message: 'Login failed' }, + })); + 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 + setState((prev) => ({ + ...prev, + isLoading: false, + error: { message: 'OTP verification not implemented yet. Use dev account.' }, + })); return false; } catch (error) { setState((prev) => ({ ...prev, isLoading: false, - error: { message: error instanceof Error ? error.message : 'Login failed' }, + error: { message: error instanceof Error ? error.message : 'Verification failed' }, })); - return false; } }, []); @@ -135,7 +184,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { }, []); return ( - + {children} ); diff --git a/services/api.ts b/services/api.ts index 83000eb..541f16d 100644 --- a/services/api.ts +++ b/services/api.ts @@ -145,6 +145,21 @@ class ApiService { await SecureStore.deleteItemAsync('userName'); await SecureStore.deleteItemAsync('privileges'); await SecureStore.deleteItemAsync('maxRole'); + await SecureStore.deleteItemAsync('userEmail'); + } + + // Save user email (for OTP auth flow) + async saveEmail(email: string): Promise { + await SecureStore.setItemAsync('userEmail', email); + } + + // Get stored email + async getStoredEmail(): Promise { + try { + return await SecureStore.getItemAsync('userEmail'); + } catch { + return null; + } } async isAuthenticated(): Promise { @@ -159,12 +174,14 @@ class ApiService { const userName = await SecureStore.getItemAsync('userName'); const privileges = await SecureStore.getItemAsync('privileges'); const maxRole = await SecureStore.getItemAsync('maxRole'); + const email = await SecureStore.getItemAsync('userEmail'); if (!userId || !userName) return null; return { user_id: parseInt(userId, 10), user_name: userName, + email: email || undefined, privileges: privileges || '', max_role: parseInt(maxRole || '0', 10), }; diff --git a/types/index.ts b/types/index.ts index f16c6bd..744fbf1 100644 --- a/types/index.ts +++ b/types/index.ts @@ -2,6 +2,7 @@ export interface User { user_id: number; user_name: string; + email?: string; max_role: number; privileges: string; }