fix(auth): advance to OTP code step in production + clear profile on logout
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 39s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m12s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 59s

- AuthScreen gated the code-entry step on devCode != null, so with real SMS
  (no devCode) it got stuck after "send". Gate on a `sent` flag instead; add
  sending state, send-failure message, "code sent" hint, change-number, and
  raise the code input cap to 6 (codes are 5 digits).
- signOut now resets the store to a fresh guest profile, and the SignalR
  service clears its cachedProfile — so the previous user's name/avatar no
  longer linger after logout.
- i18n: auth.sending / sendFailed / codeSent / invalidPhone / changeNumber.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-13 08:21:20 +03:30
parent fdf4235fbd
commit 1954992203
4 changed files with 51 additions and 12 deletions
+37 -11
View File
@@ -62,14 +62,27 @@ function PhoneForm({ onDone }: { onDone: () => void }) {
const verifyOtp = useSessionStore((s) => s.verifyOtp);
const [phone, setPhone] = useState("");
const [code, setCode] = useState("");
const [sent, setSent] = useState(false);
const [devCode, setDevCode] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const send = async () => {
if (phone.trim().length < 4) return;
const res = await requestOtp(phone.trim());
setDevCode(res.devCode ?? null);
if (phone.trim().length < 10) {
setError(t("auth.invalidPhone"));
return;
}
setBusy(true);
setError("");
try {
const res = await requestOtp(phone.trim());
setDevCode(res.devCode ?? null);
setSent(true); // advance to the code step regardless of dev/prod
} catch {
setError(t("auth.sendFailed"));
} finally {
setBusy(false);
}
};
const verify = async () => {
@@ -95,15 +108,22 @@ function PhoneForm({ onDone }: { onDone: () => void }) {
/>
</div>
{devCode == null ? (
<button onClick={send} className="btn-gold w-full rounded-xl py-3">
{t("auth.sendCode")}
</button>
{!sent ? (
<>
<button onClick={send} disabled={busy} className="btn-gold w-full rounded-xl py-3 disabled:opacity-60">
{busy ? t("auth.sending") : t("auth.sendCode")}
</button>
{error && <p className="text-rose-300 text-sm text-center">{error}</p>}
</>
) : (
<motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} className="space-y-3">
<div className="text-center text-xs text-gold-300 glass rounded-lg py-1.5">
{t("auth.devCode", { code: devCode })}
</div>
{devCode ? (
<div className="text-center text-xs text-gold-300 glass rounded-lg py-1.5">
{t("auth.devCode", { code: devCode })}
</div>
) : (
<p className="text-center text-xs text-cream/55">{t("auth.codeSent")}</p>
)}
<div>
<label className="block text-xs text-cream/55 mb-1.5">{t("auth.codeLabel")}</label>
<input
@@ -112,7 +132,7 @@ function PhoneForm({ onDone }: { onDone: () => void }) {
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t("auth.codePlaceholder")}
maxLength={4}
maxLength={6}
className="w-full rounded-xl bg-navy-900/70 gold-border px-4 py-3 text-cream text-center text-xl tracking-[0.5em] outline-none focus:ring-2 focus:ring-gold-500/40"
/>
</div>
@@ -120,6 +140,12 @@ function PhoneForm({ onDone }: { onDone: () => void }) {
<button onClick={verify} className="btn-gold w-full rounded-xl py-3">
{t("auth.verify")}
</button>
<button
onClick={() => { setSent(false); setCode(""); setError(""); }}
className="w-full text-center text-xs text-cream/45 hover:text-cream"
>
{t("auth.changeNumber")}
</button>
</motion.div>
)}
</div>