WellNuo/app/(auth)/enter-name.tsx
Sergei 7105bb72f7 Stable Light version - App Store submission
WellNuo Lite architecture:
- Simplified navigation flow with NavigationController
- Profile editing with API sync (/auth/profile endpoint)
- OTP verification improvements
- ESP WiFi provisioning setup (espProvisioning.ts)
- E2E testing infrastructure (Playwright)
- Speech recognition hooks (web/native)
- Backend auth enhancements

This is the stable version submitted to App Store.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 20:28:18 -08:00

264 lines
6.8 KiB
TypeScript

import React, { useState, useCallback, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
TouchableOpacity,
} from 'react-native';
import { router, useLocalSearchParams } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
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';
import { api } from '@/services/api';
export default function EnterNameScreen() {
const params = useLocalSearchParams<{ email: string; inviteCode: string }>();
const email = params.email || '';
const inviteCode = params.inviteCode || '';
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Debug: log when screen mounts
useEffect(() => {
console.log('[EnterName] Screen MOUNTED with params:', { email, inviteCode });
}, []);
// Debug: log when screen unmounts
useEffect(() => {
return () => {
console.log('[EnterName] Screen UNMOUNTED');
};
}, []);
const handleContinue = useCallback(async () => {
setError(null);
const trimmedFirstName = firstName.trim();
const trimmedLastName = lastName.trim();
if (!trimmedFirstName) {
setError('Please enter your first name');
return;
}
setIsLoading(true);
try {
// Update profile with name via API
console.log('[EnterName] Saving name:', { firstName: trimmedFirstName, lastName: trimmedLastName });
const response = await api.updateProfile({
firstName: trimmedFirstName,
lastName: trimmedLastName || undefined,
});
console.log('[EnterName] API response:', JSON.stringify(response));
if (!response.ok) {
console.error('[EnterName] API error:', response.error);
throw new Error(response.error?.message || 'Failed to update profile');
}
console.log('[EnterName] Name saved successfully, navigating to add-loved-one');
// Navigate to add loved one screen (onboarding step 1)
router.replace({
pathname: '/(auth)/add-loved-one',
params: {
email,
inviteCode,
},
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save name');
} finally {
setIsLoading(false);
}
}, [firstName, lastName, email, inviteCode]);
const handleSkip = () => {
// Skip entering name and go to add loved one
router.replace({
pathname: '/(auth)/add-loved-one',
params: {
email,
inviteCode,
},
});
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Skip Button */}
<View style={styles.headerRow}>
<View style={{ width: 44 }} />
<TouchableOpacity style={styles.skipButton} onPress={handleSkip}>
<Text style={styles.skipText}>Skip</Text>
</TouchableOpacity>
</View>
{/* Icon */}
<View style={styles.iconContainer}>
<View style={styles.iconCircle}>
<Ionicons name="person" size={48} color={AppColors.primary} />
</View>
</View>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>What's your name?</Text>
<Text style={styles.subtitle}>
This helps us personalize your experience
</Text>
</View>
{/* Error Message */}
{error && (
<ErrorMessage
message={error}
onDismiss={() => setError(null)}
/>
)}
{/* Form */}
<View style={styles.form}>
<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}
returnKeyType="next"
/>
<Input
label="Last Name (optional)"
placeholder="Enter your last name"
leftIcon="person-outline"
value={lastName}
onChangeText={setLastName}
autoCapitalize="words"
autoCorrect={false}
editable={!isLoading}
onSubmitEditing={handleContinue}
returnKeyType="done"
/>
</View>
{/* Continue Button */}
<View style={styles.buttonContainer}>
<Button
title="Continue"
onPress={handleContinue}
loading={isLoading}
fullWidth
size="lg"
/>
</View>
{/* Info */}
<View style={styles.infoContainer}>
<Text style={styles.infoText}>
You can update your name later in settings
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.xl,
paddingBottom: Spacing.xl,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: Spacing.xl,
},
skipButton: {
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.md,
},
skipText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
fontWeight: '500',
},
iconContainer: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
iconCircle: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: `${AppColors.primary}15`,
justifyContent: 'center',
alignItems: 'center',
},
header: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
title: {
fontSize: FontSizes['2xl'],
fontWeight: '700',
color: AppColors.textPrimary,
marginBottom: Spacing.md,
textAlign: 'center',
},
subtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
},
form: {
gap: Spacing.md,
},
buttonContainer: {
marginTop: Spacing.xl,
},
infoContainer: {
alignItems: 'center',
marginTop: Spacing.lg,
paddingHorizontal: Spacing.md,
},
infoText: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
textAlign: 'center',
},
});