diff --git a/package.json b/package.json index df73f53b..dfbb76d9 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "prettier-plugin-tailwindcss": "^0.5.14", "react": "19.1.0", "react-dom": "19.1.0", + "react-hook-form": "^7.56.4", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", "react-native-iap": "^15.2.0", diff --git a/src/components/mobile/MobileProfile.tsx b/src/components/mobile/MobileProfile.tsx index 297df389..b793380b 100644 --- a/src/components/mobile/MobileProfile.tsx +++ b/src/components/mobile/MobileProfile.tsx @@ -37,6 +37,11 @@ import { AvatarCamera } from './AvatarCamera'; import { MobileFormInput } from './MobileFormInput'; import { StatisticsDisplay } from './StatisticsDisplay'; +// Enable LayoutAnimation on Android +if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); +} + // ─── Types ─────────────────────────────────────────────────────────────────── /** @@ -247,55 +252,23 @@ export const MobileProfile: React.FC = ({ isLoading = false, }) => { const [profile, setProfile] = useState(MOCK_PROFILE); - const { - applyPrefillToFields, - persistFields, - prefillValues, - isLoading: formCacheLoading, - } = useFormCache(PROFILE_FORM_CACHE_KEYS); const unlockedCount = profile.achievements.filter(a => !a.isLocked).length; const [activeTab, setActiveTab] = useState('overview'); const [isEditing, setIsEditing] = useState(false); const [isCameraVisible, setIsCameraVisible] = useState(false); const [isSaving, setIsSaving] = useState(false); - // Edit form state - const [editName, setEditName] = useState(''); - const [editBio, setEditBio] = useState(''); - const [editEmail, setEditEmail] = useState(''); - const [editLocation, setEditLocation] = useState(''); - const [editWebsite, setEditWebsite] = useState(''); - const [formErrors, setFormErrors] = useState>({}); + const { + control, + handleSubmit, + reset, + formState: { errors: formErrors }, + } = useForm({ + defaultValues: { name: '', email: '', bio: '', location: '', website: '' }, + }); - useEffect(() => { - if (!isEditing || formCacheLoading) return; - applyPrefillToFields( - { - fullName: editName, - email: editEmail, - bio: editBio, - location: editLocation, - website: editWebsite, - }, - { - fullName: setEditName, - email: setEditEmail, - bio: setEditBio, - location: setEditLocation, - website: setEditWebsite, - } - ); - }, [ - applyPrefillToFields, - editBio, - editEmail, - editLocation, - editName, - editWebsite, - formCacheLoading, - isEditing, - prefillValues, - ]); + // Progressive disclosure: advanced profile fields collapsed by default + const [showAdvancedFields, setShowAdvancedFields] = useState(false); if (isLoading) { const bg = isDark ? '#0f172a' : '#f8fafc'; @@ -336,9 +309,6 @@ export const MobileProfile: React.FC = ({ ); } - // Progressive disclosure: advanced profile fields collapsed by default - const [showAdvancedFields, setShowAdvancedFields] = useState(false); - // Theme tokens const bg = isDark ? '#0f172a' : '#f8fafc'; const cardBg = isDark ? '#1e293b' : '#fff'; @@ -404,22 +374,21 @@ export const MobileProfile: React.FC = ({ return; } setIsSaving(true); - // Simulate API call — replace with actual service call await new Promise(resolve => setTimeout(resolve, 800)); setProfile(prev => ({ ...prev, - name: editName.trim(), - bio: editBio.trim(), - email: editEmail.trim(), - location: editLocation.trim(), - website: editWebsite.trim(), + name: data.name.trim(), + bio: data.bio.trim(), + email: data.email.trim(), + location: data.location.trim(), + website: data.website.trim(), })); - await persistFields({ - fullName: editName.trim(), - email: editEmail.trim(), - bio: editBio.trim(), - location: editLocation.trim(), - website: editWebsite.trim(), + await cacheFormValues({ + fullName: data.name.trim(), + email: data.email.trim(), + bio: data.bio.trim(), + location: data.location.trim(), + website: data.website.trim(), }); setIsSaving(false); setIsEditing(false); @@ -548,7 +517,7 @@ export const MobileProfile: React.FC = ({ void handleSave()} disabled={isSaving} accessibilityRole="button" accessibilityLabel="Save profile changes" @@ -674,38 +643,61 @@ export const MobileProfile: React.FC = ({ Edit Profile {/* ── Basic Fields (always visible) ── */} - } + ( + } + /> + )} /> - } + ( + } + /> + )} /> - ( + + )} /> {/* ── Progressive Disclosure: Advanced Details ── */} @@ -735,25 +727,37 @@ export const MobileProfile: React.FC = ({ {/* ── Advanced Fields (expandable) ── */} {showAdvancedFields && ( - } + ( + } + /> + )} /> - } + ( + } + /> + )} /> )} diff --git a/src/pages/mobile/MobileLogin.tsx b/src/pages/mobile/MobileLogin.tsx index fe1720be..cf73753c 100644 --- a/src/pages/mobile/MobileLogin.tsx +++ b/src/pages/mobile/MobileLogin.tsx @@ -11,6 +11,7 @@ import { Mail, } from 'lucide-react-native'; import React, { useEffect, useRef, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { ActivityIndicator, Alert, @@ -33,6 +34,11 @@ import authService, { AuthResult } from '../../services/mobileAuth'; import * as secureStorage from '../../services/secureStorage'; import { validateEmail, validateRequired } from '../../utils/validation'; +interface LoginFormValues { + email: string; + password: string; +} + // ─── Types ──────────────────────────────────────────────────────────────────── interface MobileLoginProps { @@ -54,17 +60,20 @@ export const MobileLogin: React.FC = ({ onRegister, isDark = false, }) => { - // ── State ──────────────────────────────────────────────────────────────── - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + // ── Form ───────────────────────────────────────────────────────────────── + const { + control, + handleSubmit, + setValue, + formState: { errors }, + } = useForm({ defaultValues: { email: '', password: '' } }); + const [showPassword, setShowPassword] = useState(false); const [rememberMe, setRememberMe] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); + const [serverError, setServerError] = useState(null); const [showBiometricModal, setShowBiometricModal] = useState(false); - const [passwordFocused, setPasswordFocused] = useState(false); - const emailRef = useRef(null); const passwordRef = useRef(null); const { @@ -97,19 +106,16 @@ export const MobileLogin: React.FC = ({ authService.getRememberedEmail(), secureStorage.isRememberMeEnabled(), ]); - if (savedEmail) setEmail(savedEmail); + if (savedEmail) setValue('email', savedEmail); if (savedRememberMe) setRememberMe(true); } loadRemembered(); - }, []); + }, [setValue]); // ── Auto-trigger biometric on mount if enabled ─────────────────────────── useEffect(() => { if (biometricEnabled && biometricAvailable) { - // Small delay so screen renders first - const timer = setTimeout(() => { - setShowBiometricModal(true); - }, 600); + const timer = setTimeout(() => setShowBiometricModal(true), 600); return () => clearTimeout(timer); } }, [biometricEnabled, biometricAvailable]); @@ -130,14 +136,13 @@ export const MobileLogin: React.FC = ({ setIsLoading(true); try { const result = await authService.login({ - email: email.trim().toLowerCase(), - password, + email: data.email.trim().toLowerCase(), + password: data.password, rememberMe, }); onLoginSuccess(result); } catch (err) { - const msg = err instanceof Error ? err.message : 'Login failed. Please try again.'; - setError(msg); + setServerError(err instanceof Error ? err.message : 'Login failed. Please try again.'); } finally { setIsLoading(false); } @@ -206,11 +211,11 @@ export const MobileLogin: React.FC = ({ {/* ── Card ── */} {/* Error banner */} - {error && ( + {displayError && ( - {error} + {displayError} )} @@ -325,7 +330,7 @@ export const MobileLogin: React.FC = ({ {/* Primary CTA */} diff --git a/src/pages/mobile/MobileRegister.tsx b/src/pages/mobile/MobileRegister.tsx index 999d1fc6..d6365068 100644 --- a/src/pages/mobile/MobileRegister.tsx +++ b/src/pages/mobile/MobileRegister.tsx @@ -1,6 +1,7 @@ import { LinearGradient } from 'expo-linear-gradient'; import { AlertCircle, BookOpen, Lock, Mail, User } from 'lucide-react-native'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { ActivityIndicator, Platform, @@ -43,7 +44,6 @@ export const MobileRegister: React.FC = ({ const [confirmPassword, setConfirmPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); - const nameRef = useRef(null); const emailRef = useRef(null); const passwordRef = useRef(null); const confirmRef = useRef(null); @@ -61,11 +61,6 @@ export const MobileRegister: React.FC = ({ }); const { scale } = useDynamicFontSize(); - const { - applyPrefillToFields, - isLoading: formCacheLoading, - prefillValues, - } = useFormCache(['fullName', 'email']); const styles = createStyles(scale); const bg = isDark ? '#0f172a' : '#f8fafc'; const cardBg = isDark ? '#1e293b' : '#fff'; @@ -86,9 +81,8 @@ export const MobileRegister: React.FC = ({ if (!validateAll({ name, email, password, confirmPassword })) return; setIsLoading(true); try { - // Registration API call would go here await new Promise(resolve => setTimeout(resolve, 1000)); - await cacheFormValues({ fullName: name.trim(), email: email.trim().toLowerCase() }); + await cacheFormValues({ fullName: data.name.trim(), email: data.email.trim().toLowerCase() }); onRegisterSuccess?.(); } finally { setIsLoading(false); @@ -200,7 +194,7 @@ export const MobileRegister: React.FC = ({ {password.length > 0 && !errors.password && ( )} - + /> {/* Confirm Password */} @@ -235,11 +229,11 @@ export const MobileRegister: React.FC = ({ {errors.confirmPassword && ( )} - + />