WellNuo/app/(auth)/login.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

194 lines
4.9 KiB
TypeScript

import React, { useState, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
TouchableOpacity,
Image,
} from 'react-native';
import { router } from 'expo-router';
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 LoginScreen() {
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleContinue = useCallback(async () => {
setError(null);
// Validate email
const trimmedEmail = email.trim().toLowerCase();
if (!trimmedEmail) {
setError('Please enter your email address');
return;
}
if (!validateEmail(trimmedEmail)) {
setError('Please enter a valid email address');
return;
}
setIsLoading(true);
try {
const response = await api.requestOTP(trimmedEmail);
if (response.ok && response.data) {
// Navigate to OTP screen with email
router.push({
pathname: '/(auth)/verify-otp',
params: {
email: trimmedEmail,
isNewUser: response.data.isNewUser ? 'true' : 'false',
},
});
} else {
setError(response.error?.message || 'Failed to send verification code');
}
} catch (err) {
setError('Something went wrong. Please try again.');
} finally {
setIsLoading(false);
}
}, [email]);
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Logo / Header */}
<View style={styles.header}>
<Image
source={require('@/assets/images/icon.png')}
style={styles.logo}
resizeMode="contain"
/>
<Text style={styles.title}>Welcome to WellNuo</Text>
<Text style={styles.subtitle}>
Enter your email to get started. We'll send you a verification code.
</Text>
</View>
{/* Form */}
<View style={styles.form}>
{error && (
<ErrorMessage
message={error}
onDismiss={() => setError(null)}
/>
)}
<Input
label="Email Address"
placeholder="Enter your email"
leftIcon="mail-outline"
value={email}
onChangeText={(text) => {
setEmail(text);
setError(null);
}}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
autoComplete="email"
editable={!isLoading}
onSubmitEditing={handleContinue}
returnKeyType="next"
/>
<Button
title="Continue"
onPress={handleContinue}
loading={isLoading}
fullWidth
size="lg"
/>
</View>
{/* Info */}
<View style={styles.infoContainer}>
<Text style={styles.infoText}>
We'll send a 6-digit code to your email for verification.
No password needed!
</Text>
</View>
{/* Version Info */}
<Text style={styles.version}>WellNuo v1.0.0</Text>
</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,
},
logo: {
width: 180,
height: 100,
marginBottom: Spacing.lg,
},
title: {
fontSize: FontSizes['2xl'],
fontWeight: '700',
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
subtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
paddingHorizontal: Spacing.md,
},
form: {
marginBottom: Spacing.xl,
},
infoContainer: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
marginBottom: Spacing.xl,
},
infoText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
textAlign: 'center',
lineHeight: 20,
},
version: {
textAlign: 'center',
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
});