WellNuo/app/(auth)/login.tsx
Sergei fe4ff1a932 Simplify DB schema (name/address single fields) + subscription flow
Database:
- Simplified beneficiary schema: single `name` field instead of first_name/last_name
- Single `address` field instead of 5 separate address columns
- Added migration 008_update_notification_settings.sql

Backend:
- Updated all beneficiaries routes for new schema
- Fixed admin routes for simplified fields
- Updated notification settings routes
- Improved stripe and webhook handlers

Frontend:
- Updated all forms to use single name/address fields
- Added new equipment-status and purchase screens
- Added BeneficiaryDetailController service
- Added subscription service
- Improved navigation and auth flow
- Various UI improvements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 10:35:15 -08:00

267 lines
7.1 KiB
TypeScript

import { Button } from '@/components/ui/Button';
import { ErrorMessage } from '@/components/ui/ErrorMessage';
import { Input } from '@/components/ui/Input';
import { AppColors, FontSizes, Spacing } from '@/constants/theme';
import { useAuth } from '@/contexts/AuthContext';
import { router } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Image,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
export default function LoginScreen() {
const { checkEmail, requestOtp, isLoading, error, clearError } = useAuth();
const [email, setEmail] = useState('');
const [inviteCode, setInviteCode] = useState('');
const [showInviteCode, setShowInviteCode] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
// Clear errors on mount
useEffect(() => {
console.log('[LoginScreen] Mounted');
clearError();
}, []);
const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleContinue = async () => {
// Clear previous errors
clearError();
setValidationError(null);
const trimmedEmail = email.trim().toLowerCase();
// Validate email
if (!trimmedEmail) {
setValidationError('Email is required');
return;
}
if (!validateEmail(trimmedEmail)) {
setValidationError('Please enter a valid email address');
return;
}
console.log('[Login] Checking email:', trimmedEmail);
// Check if email exists in database
const result = await checkEmail(trimmedEmail);
console.log('[Login] Result:', JSON.stringify(result));
// Navigate based on result
if (result.skipOtp) {
// Dev account - skip OTP
console.log('[Login] -> verify-otp (skip OTP)');
router.push({
pathname: '/(auth)/verify-otp',
params: { email: trimmedEmail, skipOtp: '1', isNewUser: '0' }
});
return;
}
// Direct OTP Flow (Streamlined)
console.log('[Login] Requesting OTP...');
const otpResult = await requestOtp(trimmedEmail);
if (otpResult.success) {
console.log('[Login] OTP sent -> verify-otp');
router.push({
pathname: '/(auth)/verify-otp',
params: {
email: trimmedEmail,
isNewUser: result.exists ? '0' : '1',
inviteCode: inviteCode.trim() || undefined,
}
});
} else {
setValidationError('Failed to send verification code. Please try again.');
}
};
const displayError = validationError || error?.message;
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<View style={styles.logoContainer}>
<Image
source={require('@/assets/logo.png')}
style={styles.logo}
resizeMode="contain"
/>
</View>
<View style={styles.header}>
<Text style={styles.title}>Welcome to WellNuo</Text>
<Text style={styles.subtitle}>
Enter your email to sign in or create an account
</Text>
</View>
<View style={styles.form}>
{displayError && (
<ErrorMessage
message={displayError}
onDismiss={() => {
clearError();
setValidationError(null);
}}
/>
)}
<Input
label="Email"
placeholder="Enter your email"
leftIcon="mail-outline"
value={email}
onChangeText={(text) => {
setEmail(text);
setValidationError(null);
}}
autoCapitalize="none"
autoCorrect={false}
keyboardType="email-address"
editable={!isLoading}
returnKeyType="next"
/>
{/* Invite Code Toggle */}
{!showInviteCode ? (
<TouchableOpacity
style={styles.inviteCodeToggle}
onPress={() => setShowInviteCode(true)}
>
<Ionicons name="gift-outline" size={18} color={AppColors.primary} />
<Text style={styles.inviteCodeToggleText}>I have an invite code</Text>
</TouchableOpacity>
) : (
<Input
label="Invite Code"
placeholder="Enter 5-digit code"
leftIcon="gift-outline"
value={inviteCode}
onChangeText={(text) => {
// Only allow digits, max 5
const code = text.replace(/\D/g, '').slice(0, 5);
setInviteCode(code);
}}
keyboardType="number-pad"
maxLength={5}
editable={!isLoading}
onSubmitEditing={handleContinue}
returnKeyType="done"
/>
)}
<Button
title="Continue"
onPress={handleContinue}
loading={isLoading}
fullWidth
size="lg"
style={styles.button}
/>
</View>
<View style={styles.infoContainer}>
<Text style={styles.infoText}>
We'll send you a verification code to confirm your email
</Text>
</View>
<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,
},
logoContainer: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
logo: {
width: 120,
height: 120,
},
header: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
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.md,
},
button: {
marginTop: Spacing.md,
},
infoContainer: {
alignItems: 'center',
marginTop: Spacing.lg,
marginBottom: Spacing.xl,
},
infoText: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
textAlign: 'center',
},
version: {
textAlign: 'center',
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 'auto',
},
inviteCodeToggle: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: Spacing.sm,
marginTop: Spacing.xs,
gap: Spacing.xs,
},
inviteCodeToggleText: {
fontSize: FontSizes.sm,
color: AppColors.primary,
fontWeight: '500',
},
});