Guests can generate a shareable GSH code for admin fulfillment; sign-in now supports Google, email, phone, and Telegram demos, with the backend spec updated. Co-authored-by: Cursor <cursoragent@cursor.com>
359 lines
11 KiB
TypeScript
359 lines
11 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { ActivityIndicator, Alert, View } from "react-native";
|
|
import { router, useLocalSearchParams } 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 { AppHeader } from "@/components/ui/Header";
|
|
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,
|
|
type AuthProvider,
|
|
} from "@/lib/stores/authStore";
|
|
|
|
const DEMO_EMAIL_PASSWORD = "gishen123";
|
|
|
|
const METHOD_META: Record<
|
|
AuthProvider,
|
|
{ title: string; subtitle: string }
|
|
> = {
|
|
google: {
|
|
title: "Google",
|
|
subtitle: "Connect with your Google account (demo).",
|
|
},
|
|
email: {
|
|
title: "Email",
|
|
subtitle: `Use any email. Demo password is ${DEMO_EMAIL_PASSWORD}.`,
|
|
},
|
|
phone: {
|
|
title: "Phone",
|
|
subtitle: `We'll send an OTP. Demo code is always ${DEMO_OTP}.`,
|
|
},
|
|
telegram: {
|
|
title: "Telegram",
|
|
subtitle: "Confirm with your Telegram username (demo).",
|
|
},
|
|
guest: {
|
|
title: "Guest",
|
|
subtitle: "Browse without an account.",
|
|
},
|
|
};
|
|
|
|
function normalizeMethod(raw: string | string[] | undefined): AuthProvider {
|
|
const v = Array.isArray(raw) ? raw[0] : raw;
|
|
if (v === "google" || v === "email" || v === "phone" || v === "telegram") {
|
|
return v;
|
|
}
|
|
return "phone";
|
|
}
|
|
|
|
export default function OtpScreen() {
|
|
const { t } = useTranslation();
|
|
const insets = useSafeAreaInsets();
|
|
const params = useLocalSearchParams<{ method?: string }>();
|
|
const method = useMemo(() => normalizeMethod(params.method), [params.method]);
|
|
const meta = METHOD_META[method];
|
|
|
|
const signIn = useAuthStore((s) => s.signIn);
|
|
const setHasOnboarded = useAuthStore((s) => s.setHasOnboarded);
|
|
const setBiometric = useAuthStore((s) => s.setBiometric);
|
|
|
|
const [name, setName] = useState("Kirukib Abebe");
|
|
const [phone, setPhone] = useState("+251911000000");
|
|
const [email, setEmail] = useState("kirukib@gmail.com");
|
|
const [password, setPassword] = useState("");
|
|
const [telegram, setTelegram] = useState("@kirukib");
|
|
const [code, setCode] = useState("");
|
|
const [sent, setSent] = useState(false);
|
|
const [bio, setBio] = useState(false);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
const finishSignIn = async (payload: {
|
|
name: string;
|
|
phone: string;
|
|
email?: string;
|
|
}) => {
|
|
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: payload.name.trim(),
|
|
phone: payload.phone.trim(),
|
|
email: payload.email?.trim() || undefined,
|
|
isGuest: false,
|
|
authProvider: method,
|
|
});
|
|
setBiometric(bio);
|
|
setHasOnboarded(true);
|
|
router.replace(routes.home);
|
|
};
|
|
|
|
const sendPhoneCode = () => {
|
|
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 verifyPhone = async () => {
|
|
if (!sent) return;
|
|
if (code.trim() !== DEMO_OTP) {
|
|
Alert.alert("Invalid OTP", `Enter the demo code ${DEMO_OTP}.`);
|
|
return;
|
|
}
|
|
await finishSignIn({
|
|
name,
|
|
phone,
|
|
email: email.trim() || undefined,
|
|
});
|
|
};
|
|
|
|
const signInEmail = async () => {
|
|
if (!name.trim()) {
|
|
Alert.alert("Missing info", "Enter your name.");
|
|
return;
|
|
}
|
|
if (!email.includes("@")) {
|
|
Alert.alert("Invalid email", "Enter a valid email address.");
|
|
return;
|
|
}
|
|
if (password.trim() !== DEMO_EMAIL_PASSWORD) {
|
|
Alert.alert(
|
|
"Wrong password",
|
|
`Demo password is ${DEMO_EMAIL_PASSWORD}.`
|
|
);
|
|
return;
|
|
}
|
|
await finishSignIn({
|
|
name,
|
|
phone: phone.trim() || "+251911000000",
|
|
email,
|
|
});
|
|
};
|
|
|
|
const signInSocial = async (provider: "google" | "telegram") => {
|
|
if (!name.trim()) {
|
|
Alert.alert("Missing info", "Enter the name to show on your profile.");
|
|
return;
|
|
}
|
|
if (provider === "telegram" && !telegram.trim()) {
|
|
Alert.alert("Telegram", "Enter your Telegram username.");
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
// Simulated OAuth / Telegram Login Widget handshake
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
setBusy(false);
|
|
const resolvedEmail =
|
|
provider === "google"
|
|
? email.trim() || `${name.split(" ")[0]?.toLowerCase() || "user"}@gmail.com`
|
|
: email.trim() || undefined;
|
|
const resolvedPhone =
|
|
phone.trim() ||
|
|
(provider === "telegram" ? "+251900000000" : "+251911000000");
|
|
await finishSignIn({
|
|
name: name.trim(),
|
|
phone: resolvedPhone,
|
|
email: resolvedEmail,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Screen>
|
|
<AppHeader title={meta.title} subtitle={meta.subtitle} />
|
|
<View
|
|
style={{
|
|
flex: 1,
|
|
paddingBottom: insets.bottom + 20,
|
|
paddingHorizontal: gutter,
|
|
}}
|
|
>
|
|
{busy ? (
|
|
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", gap: 12 }}>
|
|
<ActivityIndicator size="large" color={colors.brandText} />
|
|
<Text variant="body" color={colors.muted}>
|
|
Connecting to {method === "google" ? "Google" : "Telegram"}…
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
<>
|
|
<View style={{ marginTop: 8, gap: 12 }}>
|
|
<Field
|
|
label="Full name"
|
|
value={name}
|
|
onChangeText={setName}
|
|
icon="person-outline"
|
|
/>
|
|
|
|
{method === "phone" ? (
|
|
<>
|
|
<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}
|
|
</>
|
|
) : null}
|
|
|
|
{method === "email" ? (
|
|
<>
|
|
<Field
|
|
label="Email"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
icon="mail-outline"
|
|
/>
|
|
<Field
|
|
label="Password"
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
secure
|
|
autoCapitalize="none"
|
|
icon="lock-closed-outline"
|
|
hint={`Demo password: ${DEMO_EMAIL_PASSWORD}`}
|
|
/>
|
|
<Field
|
|
label="Phone (optional)"
|
|
value={phone}
|
|
onChangeText={setPhone}
|
|
keyboardType="phone-pad"
|
|
icon="call-outline"
|
|
/>
|
|
</>
|
|
) : null}
|
|
|
|
{method === "google" ? (
|
|
<>
|
|
<Field
|
|
label="Google email"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
icon="logo-google"
|
|
hint="Demo — no real Google OAuth"
|
|
/>
|
|
<Field
|
|
label="Phone (optional)"
|
|
value={phone}
|
|
onChangeText={setPhone}
|
|
keyboardType="phone-pad"
|
|
icon="call-outline"
|
|
/>
|
|
</>
|
|
) : null}
|
|
|
|
{method === "telegram" ? (
|
|
<>
|
|
<Field
|
|
label="Telegram username"
|
|
value={telegram}
|
|
onChangeText={setTelegram}
|
|
autoCapitalize="none"
|
|
icon="paper-plane-outline"
|
|
placeholder="@username"
|
|
/>
|
|
<Field
|
|
label="Phone linked to Telegram"
|
|
value={phone}
|
|
onChangeText={setPhone}
|
|
keyboardType="phone-pad"
|
|
icon="call-outline"
|
|
/>
|
|
</>
|
|
) : 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 }} />
|
|
|
|
{method === "phone" ? (
|
|
!sent ? (
|
|
<Button title={t("common.sendOtp")} onPress={sendPhoneCode} />
|
|
) : (
|
|
<View style={{ gap: 10 }}>
|
|
<Button title={t("common.verify")} onPress={verifyPhone} />
|
|
<Button title="Resend code" variant="ghost" onPress={sendPhoneCode} />
|
|
</View>
|
|
)
|
|
) : null}
|
|
|
|
{method === "email" ? (
|
|
<Button title="Sign in with email" icon="mail-outline" onPress={signInEmail} />
|
|
) : null}
|
|
|
|
{method === "google" ? (
|
|
<Button
|
|
title="Continue with Google"
|
|
icon="logo-google"
|
|
onPress={() => signInSocial("google")}
|
|
/>
|
|
) : null}
|
|
|
|
{method === "telegram" ? (
|
|
<Button
|
|
title="Continue with Telegram"
|
|
icon="paper-plane-outline"
|
|
onPress={() => signInSocial("telegram")}
|
|
/>
|
|
) : null}
|
|
</>
|
|
)}
|
|
</View>
|
|
</Screen>
|
|
);
|
|
}
|