fix(auth): advance to OTP code step in production + clear profile on logout
- 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:
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user