From ddfe5c7bd609050d391b5fe2b8db8725a073f650 Mon Sep 17 00:00:00 2001 From: Sergei Date: Fri, 19 Dec 2025 16:53:17 -0800 Subject: [PATCH] Add OTP-based email authentication flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace username/password login with email OTP flow - Add verify-otp screen with 6-digit code input - Add complete-profile screen for new users - Update AuthContext with refreshAuth() method - Add new API methods: requestOTP, verifyOTP, getMe, updateProfile - Backend: wellnuo.smartlaunchhub.com 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/(auth)/complete-profile.tsx | 200 ++++++++++++++++++ app/(auth)/login.tsx | 156 +++++++------- app/(auth)/verify-otp.tsx | 355 ++++++++++++++++++++++++++++++++ contexts/AuthContext.tsx | 100 +++++---- services/api.ts | 236 +++++++++++++++++++++ 5 files changed, 912 insertions(+), 135 deletions(-) create mode 100644 app/(auth)/complete-profile.tsx create mode 100644 app/(auth)/verify-otp.tsx diff --git a/app/(auth)/complete-profile.tsx b/app/(auth)/complete-profile.tsx new file mode 100644 index 0000000..0ecff88 --- /dev/null +++ b/app/(auth)/complete-profile.tsx @@ -0,0 +1,200 @@ +import React, { useState, useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + KeyboardAvoidingView, + Platform, + ScrollView, +} 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 { Input } from '@/components/ui/Input'; +import { ErrorMessage } from '@/components/ui/ErrorMessage'; +import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; + +export default function CompleteProfileScreen() { + const { email } = useLocalSearchParams<{ email: string }>(); + const { refreshAuth } = useAuth(); + + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [phone, setPhone] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleComplete = useCallback(async () => { + setError(null); + + // Validate + if (!firstName.trim()) { + setError('Please enter your first name'); + return; + } + if (!lastName.trim()) { + setError('Please enter your last name'); + return; + } + + setIsLoading(true); + + try { + const response = await api.updateProfile({ + firstName: firstName.trim(), + lastName: lastName.trim(), + phone: phone.trim() || undefined, + }); + + if (response.ok) { + // Refresh auth state and go to main app + await refreshAuth(); + router.replace('/(tabs)'); + } else { + setError(response.error?.message || 'Failed to update profile'); + } + } catch (err) { + setError('Something went wrong. Please try again.'); + } finally { + setIsLoading(false); + } + }, [firstName, lastName, phone, refreshAuth]); + + return ( + + + {/* Header */} + + + + + Complete your profile + + Just a few more details to get you started + + + + {/* Form */} + + {error && ( + setError(null)} + /> + )} + + { + setFirstName(text); + setError(null); + }} + autoCapitalize="words" + autoCorrect={false} + editable={!isLoading} + /> + + { + setLastName(text); + setError(null); + }} + autoCapitalize="words" + autoCorrect={false} + editable={!isLoading} + /> + + + +