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 verifyOtp = useSessionStore((s) => s.verifyOtp);
|
||||||
const [phone, setPhone] = useState("");
|
const [phone, setPhone] = useState("");
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
|
const [sent, setSent] = useState(false);
|
||||||
const [devCode, setDevCode] = useState<string | null>(null);
|
const [devCode, setDevCode] = useState<string | null>(null);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const send = async () => {
|
const send = async () => {
|
||||||
if (phone.trim().length < 4) return;
|
if (phone.trim().length < 10) {
|
||||||
const res = await requestOtp(phone.trim());
|
setError(t("auth.invalidPhone"));
|
||||||
setDevCode(res.devCode ?? null);
|
return;
|
||||||
|
}
|
||||||
|
setBusy(true);
|
||||||
setError("");
|
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 () => {
|
const verify = async () => {
|
||||||
@@ -95,15 +108,22 @@ function PhoneForm({ onDone }: { onDone: () => void }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{devCode == null ? (
|
{!sent ? (
|
||||||
<button onClick={send} className="btn-gold w-full rounded-xl py-3">
|
<>
|
||||||
{t("auth.sendCode")}
|
<button onClick={send} disabled={busy} className="btn-gold w-full rounded-xl py-3 disabled:opacity-60">
|
||||||
</button>
|
{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">
|
<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">
|
{devCode ? (
|
||||||
{t("auth.devCode", { code: devCode })}
|
<div className="text-center text-xs text-gold-300 glass rounded-lg py-1.5">
|
||||||
</div>
|
{t("auth.devCode", { code: devCode })}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-xs text-cream/55">{t("auth.codeSent")}</p>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-cream/55 mb-1.5">{t("auth.codeLabel")}</label>
|
<label className="block text-xs text-cream/55 mb-1.5">{t("auth.codeLabel")}</label>
|
||||||
<input
|
<input
|
||||||
@@ -112,7 +132,7 @@ function PhoneForm({ onDone }: { onDone: () => void }) {
|
|||||||
value={code}
|
value={code}
|
||||||
onChange={(e) => setCode(e.target.value)}
|
onChange={(e) => setCode(e.target.value)}
|
||||||
placeholder={t("auth.codePlaceholder")}
|
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"
|
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>
|
</div>
|
||||||
@@ -120,6 +140,12 @@ function PhoneForm({ onDone }: { onDone: () => void }) {
|
|||||||
<button onClick={verify} className="btn-gold w-full rounded-xl py-3">
|
<button onClick={verify} className="btn-gold w-full rounded-xl py-3">
|
||||||
{t("auth.verify")}
|
{t("auth.verify")}
|
||||||
</button>
|
</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>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -282,6 +282,11 @@ const fa: Dict = {
|
|||||||
"auth.toggleSignup": "حساب ندارید؟ ثبتنام کنید",
|
"auth.toggleSignup": "حساب ندارید؟ ثبتنام کنید",
|
||||||
"auth.toggleSignin": "حساب دارید؟ وارد شوید",
|
"auth.toggleSignin": "حساب دارید؟ وارد شوید",
|
||||||
"auth.invalidCode": "کد نادرست است",
|
"auth.invalidCode": "کد نادرست است",
|
||||||
|
"auth.invalidPhone": "شماره موبایل را درست وارد کنید",
|
||||||
|
"auth.sending": "در حال ارسال…",
|
||||||
|
"auth.sendFailed": "ارسال پیامک ناموفق بود، دوباره تلاش کنید",
|
||||||
|
"auth.codeSent": "کد به شماره شما پیامک شد",
|
||||||
|
"auth.changeNumber": "تغییر شماره",
|
||||||
"auth.otherSoon": "سایر روشهای ورود بهزودی فعال میشوند",
|
"auth.otherSoon": "سایر روشهای ورود بهزودی فعال میشوند",
|
||||||
|
|
||||||
"reward.title": "پاداش بازی",
|
"reward.title": "پاداش بازی",
|
||||||
@@ -646,6 +651,11 @@ const en: Dict = {
|
|||||||
"auth.toggleSignup": "No account? Sign up",
|
"auth.toggleSignup": "No account? Sign up",
|
||||||
"auth.toggleSignin": "Have an account? Sign in",
|
"auth.toggleSignin": "Have an account? Sign in",
|
||||||
"auth.invalidCode": "Invalid code",
|
"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",
|
"auth.otherSoon": "Other sign-in methods coming soon",
|
||||||
|
|
||||||
"reward.title": "Match rewards",
|
"reward.title": "Match rewards",
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ export class SignalrService implements OnlineService {
|
|||||||
async signOut() {
|
async signOut() {
|
||||||
this.session = null;
|
this.session = null;
|
||||||
this.token = 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);
|
if (typeof window !== "undefined") localStorage.removeItem(LS_SESSION);
|
||||||
await this.conn?.stop();
|
await this.conn?.stop();
|
||||||
this.conn = null;
|
this.conn = null;
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
|
|||||||
|
|
||||||
signOut: async () => {
|
signOut: async () => {
|
||||||
await getService().signOut();
|
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) => {
|
updateProfile: async (patch) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user