WellNuo/app/(auth)/complete-profile.tsx
Sergei ddfe5c7bd6 Add OTP-based email authentication flow
- 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 <noreply@anthropic.com>
2025-12-19 16:53:17 -08:00

201 lines
5.3 KiB
TypeScript

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<string | null>(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 (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<View style={styles.iconContainer}>
<Ionicons name="person-circle-outline" size={48} color={AppColors.primary} />
</View>
<Text style={styles.title}>Complete your profile</Text>
<Text style={styles.subtitle}>
Just a few more details to get you started
</Text>
</View>
{/* Form */}
<View style={styles.form}>
{error && (
<ErrorMessage
message={error}
onDismiss={() => setError(null)}
/>
)}
<Input
label="First Name"
placeholder="Enter your first name"
leftIcon="person-outline"
value={firstName}
onChangeText={(text) => {
setFirstName(text);
setError(null);
}}
autoCapitalize="words"
autoCorrect={false}
editable={!isLoading}
/>
<Input
label="Last Name"
placeholder="Enter your last name"
leftIcon="person-outline"
value={lastName}
onChangeText={(text) => {
setLastName(text);
setError(null);
}}
autoCapitalize="words"
autoCorrect={false}
editable={!isLoading}
/>
<Input
label="Phone (optional)"
placeholder="Enter your phone number"
leftIcon="call-outline"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
editable={!isLoading}
/>
<Button
title="Get Started"
onPress={handleComplete}
loading={isLoading}
fullWidth
size="lg"
/>
</View>
{/* Email info */}
<View style={styles.emailInfo}>
<Ionicons name="mail-outline" size={16} color={AppColors.textMuted} />
<Text style={styles.emailText}>{email}</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.xxl + Spacing.xl,
paddingBottom: Spacing.xl,
},
header: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
iconContainer: {
width: 80,
height: 80,
borderRadius: BorderRadius.full,
backgroundColor: `${AppColors.primary}15`,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.lg,
},
title: {
fontSize: FontSizes['2xl'],
fontWeight: '700',
color: AppColors.textPrimary,
marginBottom: Spacing.sm,
},
subtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
},
form: {
marginBottom: Spacing.xl,
},
emailInfo: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: Spacing.xs,
},
emailText: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
},
});