import { Link, router } from "expo-router"; import { useCallback, useState } from "react"; import { Alert, Image, KeyboardAvoidingView, Platform, ScrollView, Text, TouchableOpacity, View, } from "react-native"; import ReactNativeModal from "react-native-modal"; import { CustomButton } from "@/components/custom-button"; import { InputField } from "@/components/input-field"; import { OAuth } from "@/components/oauth"; import { OtpField } from "@/components/otp-field"; import { icons, images } from "@/constants"; import { ApiError, fetchAPI } from "@/lib/fetch"; import { useT } from "@/lib/i18n"; import { useSession } from "@/lib/session"; const ROLES = [ { value: "rider", titleKey: "auth.signUp.riderTitle", descKey: "auth.signUp.riderDesc", icon: "map" as const, }, { value: "driver", titleKey: "auth.signUp.driverTitle", descKey: "auth.signUp.driverDesc", icon: "dollar" as const, }, ] as const; type Role = (typeof ROLES)[number]["value"]; const SignUp = () => { const { setSession } = useSession(); const t = useT(); const [role, setRole] = useState("rider"); const [form, setForm] = useState({ name: "", email: "", phone: "", password: "", }); // "verified" is a hand-off state: it hides the code modal so its onModalHide // can bring up the success modal, since two modals can't cross-fade. const [verification, setVerification] = useState({ state: "default" as "default" | "pending" | "verified" | "success", error: "", code: "", devCode: "", busy: false, }); const onSignUpPress = async () => { if (!form.name.trim() || !form.email.trim() || !form.password) { Alert.alert( t("auth.signUp.alertMissingTitle"), t("auth.signUp.alertMissingBody"), ); return; } if (form.phone.trim() && !/^[0-9\s\-()+.]+$/.test(form.phone)) { Alert.alert( t("auth.signUp.alertPhoneTitle"), t("auth.signUp.alertPhoneBody"), ); return; } try { const response = await fetchAPI("/(api)/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: form.name, email: form.email, phone: form.phone.trim(), password: form.password, role, }), }); setVerification({ state: "pending", error: "", code: "", busy: false, devCode: (response as { data?: { devCode?: string } })?.data?.devCode ?? "", }); setForm((prevForm) => ({ ...prevForm, password: "", })); } catch (err: any) { setForm((prevForm) => ({ ...prevForm, password: "", })); Alert.alert( t("auth.signUp.alertErrorTitle"), err instanceof ApiError && err.status < 500 ? err.message : t("auth.signUp.alertErrorFallback"), ); } }; const onPressVerify = useCallback( async (code: string) => { if (!/^\d{6}$/.test(code)) { setVerification((prevVerification) => ({ ...prevVerification, error: t("auth.signUp.errCode"), })); return; } setVerification((prevVerification) => // Guard the double submit that auto-verify + a button tap would cause. prevVerification.busy ? prevVerification : { ...prevVerification, busy: true, error: "" }, ); try { const response = await fetchAPI("/(api)/auth/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: form.email, code }), }); await setSession(response.data); setVerification((prevVerification) => ({ ...prevVerification, state: "verified", busy: false, })); } catch (err: any) { // Stay on the code modal so the user can retry; only a real success // advances the flow. setVerification((prevVerification) => ({ ...prevVerification, code: "", busy: false, error: err instanceof ApiError && err.status < 500 ? err.message : t("auth.signUp.errVerify"), })); } }, [form.email, setSession, t], ); return ( {t("auth.signUp.carAlt")} {t("auth.signUp.createAccount")} {t("auth.signUp.howUse")} {ROLES.map((option) => { const selected = role === option.value; return ( setRole(option.value)} activeOpacity={0.8} className={`flex-1 justify-center rounded-2xl border p-4 ${ selected ? "border-primary-500 bg-primary-500/10" : "border-neutral-100 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-900" }`} > {t(`auth.signUp.${option.value {t(option.titleKey)} {t(option.descKey)} ); })} setForm((prevForm) => ({ ...prevForm, name: value, })) } autoCapitalize="words" /> setForm((prevForm) => ({ ...prevForm, email: value, })) } keyboardType="email-address" /> setForm((prevForm) => ({ ...prevForm, phone: value, })) } keyboardType="phone-pad" /> setForm((prevForm) => ({ ...prevForm, password: value, })) } /> {t("auth.signUp.haveAccount")} {t("auth.signUp.signInLink")} setVerification((prevVerification) => prevVerification.state === "verified" ? { ...prevVerification, state: "success" } : prevVerification, ) } isVisible={verification.state === "pending"} > {t("auth.signUp.verify.title")} {t("auth.signUp.verify.body", { email: form.email })} {verification.devCode ? ( {t("auth.signUp.verify.devBanner")} {verification.devCode} ) : null} setVerification((prevVerification) => ({ ...prevVerification, code, error: "", })) } onComplete={onPressVerify} /> {verification.error ? ( {verification.error} ) : null} onPressVerify(verification.code)} disabled={verification.busy} className="mt-5 bg-emerald-500" /> {t("auth.signUp.verify.checkAlt")} {t("auth.signUp.verified.title")} {t("auth.signUp.verified.body")} router.push("/")} className="mt-5" /> ); }; export default SignUp;