From 2dc0cf742582934e0637b6fccab42ab1e7c2dd30 Mon Sep 17 00:00:00 2001 From: chucksentertainment-hash Date: Sat, 30 May 2026 11:07:48 +0000 Subject: [PATCH] feat: migrate forms to react-hook-form for minimal re-renders - Install react-hook-form@7.56.4 - MobileFormInput: make value/onChangeText optional to support Controller - MobileLogin: replace useState fields with useForm + Controller; move server errors to separate serverError state; load remembered email via setValue - MobileRegister: replace useState fields + manual validate() with useForm + Controller; use watch('password') for confirm validation; remove useFormCache prefill (simplified) - MobileProfile edit form: replace five useState edit fields with useForm + Controller; replace manual validateForm() with RHF rules; replace persistFields with cacheFormValues on submit Resolves: form typing lag caused by full re-render on every keystroke --- package-lock.json | 91 ++----- package.json | 3 +- src/components/mobile/MobileFormInput.tsx | 7 +- src/components/mobile/MobileProfile.tsx | 288 ++++++++++---------- src/pages/mobile/MobileLogin.tsx | 190 +++++++------- src/pages/mobile/MobileRegister.tsx | 305 +++++++++++----------- 6 files changed, 417 insertions(+), 467 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26c0cd54..a858f5c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "teachlink_mobile", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "teachlink_mobile", - "version": "1.2.0", + "version": "1.3.0", "dependencies": { "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "2.2.0", @@ -49,6 +49,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", @@ -5263,9 +5264,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5280,9 +5278,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5297,9 +5292,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5314,9 +5306,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5331,9 +5320,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5348,9 +5334,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5365,9 +5348,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5382,9 +5362,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5399,9 +5376,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5416,9 +5390,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10414,9 +10385,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10437,9 +10405,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10460,9 +10425,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14378,9 +14340,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14401,9 +14360,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14424,9 +14380,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14447,9 +14400,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14510,9 +14460,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -17527,6 +17474,22 @@ "react": ">=17.0.0" } }, + "node_modules/react-hook-form": { + "version": "7.56.4", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.4.tgz", + "integrity": "sha512-Rob7Ftz2vyZ/ZGsQZPaRdIefkgOSrQSPXfqBdvOPwJfoGnjwRJUs7EM7Kc1mcoDv3NOtqBzPGbcMB8CGn9CKgw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", @@ -19705,22 +19668,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tar": { "version": "7.5.15", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", diff --git a/package.json b/package.json index b4c3c01d..906e8e93 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,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", @@ -100,6 +101,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@jest/globals": "^29.7.0", "@lhci/cli": "^0.15.1", "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "^13.3.3", @@ -107,7 +109,6 @@ "@types/babel__generator": "^7.27.0", "@types/babel__template": "^7.4.4", "@types/babel__traverse": "^7.28.0", - "@jest/globals": "^29.7.0", "@types/jest": "29.5.14", "@types/node": "^25.0.10", "@types/react": "~19.1.0", diff --git a/src/components/mobile/MobileFormInput.tsx b/src/components/mobile/MobileFormInput.tsx index d8b48b62..70966db9 100644 --- a/src/components/mobile/MobileFormInput.tsx +++ b/src/components/mobile/MobileFormInput.tsx @@ -11,15 +11,16 @@ import { import { AppText as Text } from '../common/AppText'; /** - * Props for the MobileFormInput component + * Props for the MobileFormInput component. + * Supports both uncontrolled (value/onChangeText) and react-hook-form Controller usage. */ interface MobileFormInputProps extends TextInputProps { /** Label text for the input field */ label: string; /** Current value of the input */ - value: string; + value?: string; /** Callback when the input value changes */ - onChangeText: (text: string) => void; + onChangeText?: (text: string) => void; /** Error message to display */ error?: string; /** Hint text to display next to the label */ diff --git a/src/components/mobile/MobileProfile.tsx b/src/components/mobile/MobileProfile.tsx index cf6ff355..0f77d5bf 100644 --- a/src/components/mobile/MobileProfile.tsx +++ b/src/components/mobile/MobileProfile.tsx @@ -17,10 +17,10 @@ import { Users, X, } from 'lucide-react-native'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { ActivityIndicator, - Animated, LayoutAnimation, Platform, SafeAreaView, @@ -30,21 +30,21 @@ import { UIManager, View, } from 'react-native'; -import { useFormCache } from '../../hooks/useFormCache'; -import { PROFILE_FORM_CACHE_KEYS } from '../../services/formCache'; -// Enable LayoutAnimation on Android -if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { - UIManager.setLayoutAnimationEnabledExperimental(true); -} import { Achievement, AchievementBadges } from './AchievementBadges'; import { AvatarCamera } from './AvatarCamera'; import { MobileFormInput } from './MobileFormInput'; import { StatisticsDisplay } from './StatisticsDisplay'; +import { cacheFormValues } from '../../services/formCache'; import { AppText as Text } from '../common/AppText'; import { CachedImage } from '../ui/CachedImage'; import { Skeleton } from '../ui/Skeleton'; +// Enable LayoutAnimation on Android +if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); +} + // ─── Types ─────────────────────────────────────────────────────────────────── /** @@ -255,55 +255,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'; @@ -344,9 +312,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'; @@ -363,29 +328,14 @@ export const MobileProfile: React.FC = ({ .slice(0, 2); const handleStartEdit = () => { - setEditName(profile.name); - setEditBio(profile.bio); - setEditEmail(profile.email); - setEditLocation(profile.location); - setEditWebsite(profile.website); - applyPrefillToFields( - { - fullName: profile.name, - email: profile.email, - bio: profile.bio, - location: profile.location, - website: profile.website, - }, - { - fullName: setEditName, - email: setEditEmail, - bio: setEditBio, - location: setEditLocation, - website: setEditWebsite, - } - ); - setFormErrors({}); - setShowAdvancedFields(false); // reset disclosure state on each edit session + reset({ + name: profile.name, + email: profile.email, + bio: profile.bio, + location: profile.location, + website: profile.website, + }); + setShowAdvancedFields(false); setIsEditing(true); }; @@ -394,45 +344,30 @@ export const MobileProfile: React.FC = ({ setShowAdvancedFields(prev => !prev); }; - const validateForm = (): Record => { - const errors: Record = {}; - if (!editName.trim()) errors.name = 'Name is required'; - if (!editEmail.trim()) errors.email = 'Email is required'; - else if (!/\S+@\S+\.\S+/.test(editEmail)) errors.email = 'Enter a valid email address'; - return errors; - }; - - const handleSave = async () => { - const errors = validateForm(); - if (Object.keys(errors).length > 0) { - setFormErrors(errors); - return; - } + const handleSave = handleSubmit(async data => { 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); - }; + }); const handleCancelEdit = () => { setIsEditing(false); - setFormErrors({}); }; const handleAvatarConfirm = (uri: string) => { @@ -548,7 +483,7 @@ export const MobileProfile: React.FC = ({ void handleSave()} disabled={isSaving} accessibilityRole="button" accessibilityLabel="Save profile changes" @@ -674,38 +609,61 @@ export const MobileProfile: React.FC = ({ Edit Profile {/* ── Basic Fields (always visible) ── */} - } + ( + } + /> + )} /> - } + ( + } + /> + )} /> - ( + + )} /> {/* ── Progressive Disclosure: Advanced Details ── */} @@ -717,39 +675,55 @@ export const MobileProfile: React.FC = ({ onPress={handleToggleAdvancedFields} activeOpacity={0.7} accessibilityRole="button" - accessibilityLabel={showAdvancedFields ? 'Hide advanced details' : 'Show advanced details'} + accessibilityLabel={ + showAdvancedFields ? 'Hide advanced details' : 'Show advanced details' + } accessibilityState={{ expanded: showAdvancedFields }} > {showAdvancedFields ? 'Hide Advanced Details' : 'Advanced Details'} - {showAdvancedFields - ? - : } + {showAdvancedFields ? ( + + ) : ( + + )} {/* ── Advanced Fields (expandable) ── */} {showAdvancedFields && ( - } + ( + } + /> + )} /> - } + ( + } + /> + )} /> )} diff --git a/src/pages/mobile/MobileLogin.tsx b/src/pages/mobile/MobileLogin.tsx index 7c7ccc68..eb7ae87f 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 { @@ -87,19 +96,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]); @@ -113,30 +119,18 @@ export const MobileLogin: React.FC = ({ const accentColor = '#19c3e6'; // ── Password login ─────────────────────────────────────────────────────── - const handlePasswordLogin = async () => { - const emailCheck = validateEmail(email); - if (!emailCheck.valid) { - setError(emailCheck.message ?? 'Invalid email.'); - return; - } - const passwordCheck = validateRequired(password, 'Password'); - if (!passwordCheck.valid) { - setError(passwordCheck.message ?? 'Password is required.'); - return; - } - - setError(null); + const onSubmit = async (data: LoginFormValues) => { + setServerError(null); 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); } @@ -165,12 +159,7 @@ export const MobileLogin: React.FC = ({ ); }; - // ── Input border colors ────────────────────────────────────────────────── - const passwordBorder = error?.toLowerCase().includes('password') - ? '#ef4444' - : passwordFocused - ? accentColor - : borderColor; + const displayError = serverError ?? errors.email?.message ?? errors.password?.message; // ───────────────────────────────────────────────────────────────────────── return ( @@ -205,30 +194,42 @@ export const MobileLogin: React.FC = ({ {/* ── Card ── */} {/* Error banner */} - {error && ( + {displayError && ( - {error} + {displayError} )} - { - setEmail(v); - setError(null); + { + const r = validateEmail(v); + return r.valid || (r.message ?? 'Invalid email.'); + }, }} - placeholder="you@example.com" - isDark={isDark} - cacheKey="email" - keyboardType="email-address" - autoCapitalize="none" - leftIcon={} - onSubmitEditing={() => passwordRef.current?.focus()} + render={({ field: { onChange, value } }) => ( + { + onChange(v); + setServerError(null); + }} + placeholder="you@example.com" + isDark={isDark} + cacheKey="email" + keyboardType="email-address" + autoCapitalize="none" + leftIcon={} + onSubmitEditing={() => passwordRef.current?.focus()} + error={errors.email?.message} + /> + )} /> {/* Password */} @@ -248,42 +249,55 @@ export const MobileLogin: React.FC = ({ )} - - - { - setPassword(v); - setError(null); - }} - placeholder="Enter your password" - placeholderTextColor={isDark ? '#475569' : '#94a3b8'} - secureTextEntry={!showPassword} - autoComplete="current-password" - returnKeyType="go" - onFocus={() => setPasswordFocused(true)} - onBlur={() => setPasswordFocused(false)} - onSubmitEditing={handlePasswordLogin} - /> - setShowPassword(s => !s)} - hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} - > - {showPassword ? ( - - ) : ( - - )} - - + { + const r = validateRequired(v, 'Password'); + return r.valid || (r.message ?? 'Password is required.'); + }, + }} + render={({ field: { onChange, value } }) => ( + + + { + onChange(v); + setServerError(null); + }} + placeholder="Enter your password" + placeholderTextColor={isDark ? '#475569' : '#94a3b8'} + secureTextEntry={!showPassword} + autoComplete="current-password" + returnKeyType="go" + onSubmitEditing={handleSubmit(onSubmit)} + /> + setShowPassword(s => !s)} + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} + > + {showPassword ? ( + + ) : ( + + )} + + + )} + /> {/* Remember Me */} @@ -306,7 +320,7 @@ export const MobileLogin: React.FC = ({ {/* Primary CTA */} diff --git a/src/pages/mobile/MobileRegister.tsx b/src/pages/mobile/MobileRegister.tsx index ea1b792b..44149bf7 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, KeyboardAvoidingView, @@ -16,7 +17,6 @@ import { import { MobileFormInput } from '../../components/mobile/MobileFormInput'; import { useDynamicFontSize } from '../../hooks/useDynamicFontSize'; -import { useFormCache } from '../../hooks/useFormCache'; import { cacheFormValues } from '../../services/formCache'; import { getPasswordStrength, @@ -32,11 +32,11 @@ interface MobileRegisterProps { isDark?: boolean; } -interface FieldErrors { - name?: string; - email?: string; - password?: string; - confirmPassword?: string; +interface RegisterFormValues { + name: string; + email: string; + password: string; + confirmPassword: string; } export const MobileRegister: React.FC = ({ @@ -44,24 +44,22 @@ export const MobileRegister: React.FC = ({ onLogin, isDark = false, }) => { - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [errors, setErrors] = useState({}); + const { + control, + handleSubmit, + watch, + formState: { errors }, + } = useForm({ + defaultValues: { name: '', email: '', password: '', confirmPassword: '' }, + }); + const [isLoading, setIsLoading] = useState(false); - const nameRef = useRef(null); const emailRef = useRef(null); const passwordRef = useRef(null); const confirmRef = useRef(null); 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'; @@ -71,48 +69,20 @@ export const MobileRegister: React.FC = ({ const accentColor = '#19c3e6'; const inputBg = isDark ? '#0f172a' : '#f8fafc'; - const passwordStrength = getPasswordStrength(password); - - useEffect(() => { - if (formCacheLoading) return; - applyPrefillToFields({ fullName: name, email }, { fullName: setName, email: setEmail }); - }, [applyPrefillToFields, email, formCacheLoading, name, prefillValues]); - - function clearFieldError(field: keyof FieldErrors) { - setErrors(prev => ({ ...prev, [field]: undefined })); - } - - function validate(): boolean { - const nameCheck = validateName(name); - const emailCheck = validateEmail(email); - const passwordCheck = validatePassword(password); - const confirmCheck = validateConfirmPassword(password, confirmPassword); + const passwordValue = watch('password'); + const passwordStrength = getPasswordStrength(passwordValue); - const next: FieldErrors = {}; - if (!nameCheck.valid) next.name = nameCheck.message; - if (!emailCheck.valid) next.email = emailCheck.message; - if (!passwordCheck.valid) next.password = passwordCheck.message; - if (!confirmCheck.valid) next.confirmPassword = confirmCheck.message; - - setErrors(next); - return Object.keys(next).length === 0; - } - - const handleRegister = async () => { - if (!validate()) return; + const onSubmit = async (data: RegisterFormValues) => { 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); } }; - const fieldBorder = (field: keyof FieldErrors) => (errors[field] ? '#ef4444' : borderColor); - return ( = ({ - { - setName(v); - clearFieldError('name'); + { + const r = validateName(v); + return r.valid || (r.message ?? 'Name is required.'); + }, }} - placeholder="Your full name" - required - error={errors.name} - isDark={isDark} - cacheKey="fullName" - keyboardType="default" - autoCapitalize="words" - leftIcon={} - onSubmitEditing={() => emailRef.current?.focus()} + render={({ field: { onChange, value } }) => ( + } + onSubmitEditing={() => emailRef.current?.focus()} + /> + )} /> - { - setEmail(v); - clearFieldError('email'); + { + const r = validateEmail(v); + return r.valid || (r.message ?? 'Invalid email.'); + }, }} - placeholder="you@example.com" - required - error={errors.email} - isDark={isDark} - cacheKey="email" - keyboardType="email-address" - autoCapitalize="none" - leftIcon={} - onSubmitEditing={() => passwordRef.current?.focus()} + render={({ field: { onChange, value } }) => ( + } + onSubmitEditing={() => passwordRef.current?.focus()} + /> + )} /> {/* Password */} - - - Password * - - - - { - setPassword(v); - clearFieldError('password'); - }} - placeholder="Min 8 chars, 1 uppercase, 1 number" - placeholderTextColor={isDark ? '#475569' : '#94a3b8'} - secureTextEntry - autoComplete="new-password" - returnKeyType="next" - onSubmitEditing={() => confirmRef.current?.focus()} - /> - - {errors.password && } - {password.length > 0 && !errors.password && ( - + { + const r = validatePassword(v); + return r.valid || (r.message ?? 'Invalid password.'); + }, + }} + render={({ field: { onChange, value } }) => ( + + + Password * + + + + confirmRef.current?.focus()} + /> + + {errors.password && ( + + )} + {value.length > 0 && !errors.password && ( + + )} + )} - + /> {/* Confirm Password */} - - - Confirm Password * - - - - { - setConfirmPassword(v); - clearFieldError('confirmPassword'); - }} - placeholder="Repeat your password" - placeholderTextColor={isDark ? '#475569' : '#94a3b8'} - secureTextEntry - autoComplete="new-password" - returnKeyType="go" - onSubmitEditing={handleRegister} - /> - - {errors.confirmPassword && ( - + { + const r = validateConfirmPassword(passwordValue, v); + return r.valid || (r.message ?? 'Passwords do not match.'); + }, + }} + render={({ field: { onChange, value } }) => ( + + + Confirm Password * + + + + + + {errors.confirmPassword && ( + + )} + )} - + />