This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Mob/app/(root)/auth/otp.tsx
kirukib 858972b5ca Add multi-provider login and guest pharmacist order codes.
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>
2026-08-07 16:11:23 +03:00

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>
);
}