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>
+10
View File
@@ -282,6 +282,11 @@ const fa: Dict = {
"auth.toggleSignup": "حساب ندارید؟ ثبت‌نام کنید",
"auth.toggleSignin": "حساب دارید؟ وارد شوید",
"auth.invalidCode": "کد نادرست است",
"auth.invalidPhone": "شماره موبایل را درست وارد کنید",
"auth.sending": "در حال ارسال…",
"auth.sendFailed": "ارسال پیامک ناموفق بود، دوباره تلاش کنید",
"auth.codeSent": "کد به شماره شما پیامک شد",
"auth.changeNumber": "تغییر شماره",
"auth.otherSoon": "سایر روش‌های ورود به‌زودی فعال می‌شوند",
"reward.title": "پاداش بازی",
@@ -646,6 +651,11 @@ const en: Dict = {
"auth.toggleSignup": "No account? Sign up",
"auth.toggleSignin": "Have an account? Sign in",
"auth.invalidCode": "Invalid code",
"auth.invalidPhone": "Enter a valid mobile number",
"auth.sending": "Sending…",
"auth.sendFailed": "Couldn't send the SMS, try again",
"auth.codeSent": "Code sent to your number",
"auth.changeNumber": "Change number",
"auth.otherSoon": "Other sign-in methods coming soon",
"reward.title": "Match rewards",
+1
View File
@@ -231,6 +231,7 @@ export class SignalrService implements OnlineService {
async signOut() {
this.session = null;
this.token = null;
this.cachedProfile = null; // drop the signed-in profile so it can't leak post-logout
if (typeof window !== "undefined") localStorage.removeItem(LS_SESSION);
await this.conn?.stop();
this.conn = null;
+3 -1
View File
@@ -88,7 +88,9 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
signOut: async () => {
await getService().signOut();
set({ session: null, isAuthed: false });
// Reset to a fresh guest profile so the old name/avatar don't linger.
const profile = await getService().getProfile();
set({ session: null, isAuthed: false, profile });
},
updateProfile: async (patch) => {