165 lines
3.7 KiB
TypeScript
165 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useSearchParams, useRouter } from "next/navigation";
|
|
import AuthShell from "@/components/AuthShell";
|
|
import { api, setToken } from "@/lib/api";
|
|
import { useLanguage } from "@/contexts/LanguageContext";
|
|
|
|
function Inner() {
|
|
const sp = useSearchParams();
|
|
const router = useRouter();
|
|
const { setLang } = useLanguage();
|
|
|
|
const email = sp.get("email") ?? "";
|
|
|
|
const [otp, setOtp] = useState("");
|
|
const [err, setErr] = useState("");
|
|
const [msg, setMsg] = useState("");
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function submit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
|
|
setBusy(true);
|
|
setErr("");
|
|
|
|
try {
|
|
const res = await api("/auth/verify-login-otp", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
email,
|
|
otp,
|
|
}),
|
|
});
|
|
|
|
setToken(res.access_token);
|
|
|
|
const org = await api("/organization");
|
|
|
|
if (org.language) {
|
|
setLang(org.language);
|
|
localStorage.setItem("language", org.language);
|
|
}
|
|
|
|
if (org.timezone) {
|
|
localStorage.setItem("timezone", org.timezone);
|
|
}
|
|
|
|
const me = await api("/me");
|
|
|
|
if (me.is_product_admin) {
|
|
router.push("/admin");
|
|
} else if (!me.has_org) {
|
|
router.push("/onboarding");
|
|
} else {
|
|
router.push("/dashboard");
|
|
}
|
|
|
|
} catch (e: any) {
|
|
setErr(
|
|
typeof e.detail === "string"
|
|
? e.detail
|
|
: "Invalid or expired OTP"
|
|
);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
async function resend() {
|
|
setErr("");
|
|
setMsg("");
|
|
|
|
try {
|
|
await api("/auth/login", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
email,
|
|
resend: true,
|
|
}),
|
|
});
|
|
|
|
setMsg("A new OTP has been sent.");
|
|
} catch {
|
|
setErr("Unable to resend OTP.");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<AuthShell
|
|
title="Verify Login"
|
|
subtitle={`Enter the 6-digit OTP sent to ${email}`}
|
|
>
|
|
<form onSubmit={submit}>
|
|
<label className="block mb-4">
|
|
<span className="text-sm font-medium text-slate-700">
|
|
Login OTP
|
|
</span>
|
|
|
|
<input
|
|
value={otp}
|
|
onChange={(e) =>
|
|
setOtp(
|
|
e.target.value
|
|
.replace(/\D/g, "")
|
|
.slice(0, 6)
|
|
)
|
|
}
|
|
inputMode="numeric"
|
|
placeholder="123456"
|
|
className="mt-1 w-full text-center tracking-[0.5em] text-lg px-3 py-3 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-brand-500/30"
|
|
/>
|
|
</label>
|
|
|
|
{err && (
|
|
<p className="text-sm text-rose-600 mb-3">
|
|
{err}
|
|
</p>
|
|
)}
|
|
|
|
{msg && (
|
|
<p className="text-sm text-emerald-600 mb-3">
|
|
{msg}
|
|
</p>
|
|
)}
|
|
|
|
<button
|
|
disabled={busy || otp.length < 6}
|
|
className="w-full bg-brand-600 text-white py-3 rounded-full font-semibold disabled:opacity-50"
|
|
>
|
|
{busy ? "Verifying..." : "Verify OTP"}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="flex justify-between mt-4 text-sm">
|
|
<button
|
|
onClick={resend}
|
|
className="text-brand-600"
|
|
>
|
|
Resend OTP
|
|
</button>
|
|
|
|
<Link
|
|
href="/login"
|
|
className="text-slate-500"
|
|
>
|
|
Back to login
|
|
</Link>
|
|
</div>
|
|
|
|
<p className="text-xs text-slate-400 mt-4">
|
|
Check your email inbox (and spam folder) for the login OTP.
|
|
</p>
|
|
</AuthShell>
|
|
);
|
|
}
|
|
|
|
export default function LoginOTP() {
|
|
return (
|
|
<Suspense>
|
|
<Inner />
|
|
</Suspense>
|
|
);
|
|
} |