Ship Expo Router screens, seeded stores, and polished UX so auth, shop, checkout, prescriptions, loyalty, family, and account processes work end-to-end offline. Co-authored-by: Cursor <cursoragent@cursor.com>
136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
import { useState } from "react";
|
|
import { Alert, View } from "react-native";
|
|
import { router } from "expo-router";
|
|
import * as LocalAuthentication from "expo-local-authentication";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Field, SwitchRow } from "@/components/ui/Field";
|
|
import { Card } from "@/components/ui/Card";
|
|
import { Screen } from "@/components/ui/Screen";
|
|
import { Text } from "@/components/ui/Text";
|
|
import { DEMO_OTP } from "@/lib/commerce";
|
|
import { colors, gutter } from "@/lib/theme";
|
|
import { routes } from "@/lib/routes";
|
|
import { useAuthStore } from "@/lib/stores/authStore";
|
|
|
|
export default function OtpScreen() {
|
|
const { t } = useTranslation();
|
|
const insets = useSafeAreaInsets();
|
|
const signIn = useAuthStore((s) => s.signIn);
|
|
const setHasOnboarded = useAuthStore((s) => s.setHasOnboarded);
|
|
const setBiometric = useAuthStore((s) => s.setBiometric);
|
|
const [phone, setPhone] = useState("+251911000000");
|
|
const [name, setName] = useState("Kirukib Abebe");
|
|
const [email, setEmail] = useState("kirukib@example.com");
|
|
const [code, setCode] = useState("");
|
|
const [sent, setSent] = useState(false);
|
|
const [bio, setBio] = useState(false);
|
|
|
|
const sendCode = () => {
|
|
if (!name.trim() || phone.replace(/\D/g, "").length < 9) {
|
|
Alert.alert("Missing info", "Enter your name and a valid phone number.");
|
|
return;
|
|
}
|
|
setSent(true);
|
|
Alert.alert(
|
|
"Code sent (demo)",
|
|
`Use OTP ${DEMO_OTP} to verify. No real SMS is sent in this build.`
|
|
);
|
|
};
|
|
|
|
const verify = async () => {
|
|
if (!sent) return;
|
|
if (code.trim() !== DEMO_OTP) {
|
|
Alert.alert("Invalid OTP", `Enter the demo code ${DEMO_OTP}.`);
|
|
return;
|
|
}
|
|
if (bio) {
|
|
const hw = await LocalAuthentication.hasHardwareAsync();
|
|
if (hw) {
|
|
const res = await LocalAuthentication.authenticateAsync({
|
|
promptMessage: "Enable biometric unlock",
|
|
});
|
|
if (!res.success) {
|
|
Alert.alert("Biometric", "Skipped — you can enable later in profile.");
|
|
}
|
|
}
|
|
}
|
|
signIn({
|
|
name: name.trim(),
|
|
phone: phone.trim(),
|
|
email: email.trim() || undefined,
|
|
isGuest: false,
|
|
});
|
|
setBiometric(bio);
|
|
setHasOnboarded(true);
|
|
router.replace(routes.home);
|
|
};
|
|
|
|
return (
|
|
<Screen>
|
|
<View
|
|
style={{
|
|
flex: 1,
|
|
paddingTop: insets.top + 24,
|
|
paddingBottom: insets.bottom + 20,
|
|
paddingHorizontal: gutter,
|
|
}}
|
|
>
|
|
<Text variant="h1">{t("common.signIn")}</Text>
|
|
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 8 }}>
|
|
{t("common.otpHint")} Demo OTP is always {DEMO_OTP}.
|
|
</Text>
|
|
<View style={{ marginTop: 20, gap: 12 }}>
|
|
<Field label="Full name" value={name} onChangeText={setName} icon="person-outline" />
|
|
<Field
|
|
label="Phone"
|
|
value={phone}
|
|
onChangeText={setPhone}
|
|
keyboardType="phone-pad"
|
|
icon="call-outline"
|
|
/>
|
|
<Field
|
|
label="Email (optional)"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
icon="mail-outline"
|
|
/>
|
|
{sent ? (
|
|
<Field
|
|
label="OTP code"
|
|
value={code}
|
|
onChangeText={setCode}
|
|
keyboardType="number-pad"
|
|
placeholder={DEMO_OTP}
|
|
icon="key-outline"
|
|
hint={`Demo code: ${DEMO_OTP}`}
|
|
/>
|
|
) : null}
|
|
<Card padded={false}>
|
|
<SwitchRow
|
|
icon="finger-print-outline"
|
|
title={t("common.enableBiometric")}
|
|
subtitle="Unlock the app faster next time"
|
|
value={bio}
|
|
onValueChange={setBio}
|
|
last
|
|
/>
|
|
</Card>
|
|
</View>
|
|
<View style={{ flex: 1 }} />
|
|
{!sent ? (
|
|
<Button title={t("common.sendOtp")} onPress={sendCode} />
|
|
) : (
|
|
<View style={{ gap: 10 }}>
|
|
<Button title={t("common.verify")} onPress={verify} />
|
|
<Button title="Resend code" variant="ghost" onPress={sendCode} />
|
|
</View>
|
|
)}
|
|
</View>
|
|
</Screen>
|
|
);
|
|
}
|