From b5d97c0cda3555a1a03193776b1a3ea5b8550458 Mon Sep 17 00:00:00 2001 From: AliReza Date: Sun, 27 Sep 2026 22:31:06 +0330 Subject: [PATCH] open the code step at once and let the SMS request finish in the background Kavenegar sends the SMS early but answers our API up to 16 s later (2026-09-27), and the login screen waited for that answer. The server stores the code before calling Kavenegar, so the code works immediately (checked against a local server with an 8 s send: login 200 at 1.5 s). Real failures still show on the code step, and resend unlocks at once. Co-Authored-By: Claude Opus 5.5 --- src/screens/LoginScreen.tsx | 46 ++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx index 6b097ce..105220e 100644 --- a/src/screens/LoginScreen.tsx +++ b/src/screens/LoginScreen.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { ChevronRight, Phone } from 'lucide-react'; -import { api } from '@/api'; +import { api, ApiError } from '@/api'; import { InlineError, PrimaryButton } from '@/components/ui'; import { SeyarMascot } from '@/mascots/SeyarMascot'; import { friendlyError } from '@/lib/errors'; @@ -30,9 +30,13 @@ export function LoginScreen({ const [phone, setPhone] = useState(''); const [otp, setOtp] = useState(''); const [devOtp, setDevOtp] = useState(null); - const [loading, setLoading] = useState<'otp' | 'login' | 'guest' | null>(null); + const [loading, setLoading] = useState<'login' | 'guest' | null>(null); const [error, setError] = useState(''); const [resendIn, setResendIn] = useState(0); + /** درخواستِ پیامک هنوز جواب نداده؛ جدا از `loading` تا واردکردنِ کد قفل نشود. */ + const [sending, setSending] = useState(false); + /** شماره‌ی آخرین درخواست؛ جوابِ دیرِ درخواستِ قدیمی‌تر نادیده گرفته می‌شود. */ + const attempt = useRef(0); const otpInput = useRef(null); const phoneValid = /^09\d{9}$/.test(phone); @@ -48,20 +52,37 @@ export function LoginScreen({ if (step === 'otp') otpInput.current?.focus(); }, [step]); + /** + * صفحه‌ی کد فوراً باز می‌شود و درخواست در پس‌زمینه جلو می‌رود. سرور کد را پیش از + * تماس با کاوه‌نگار ذخیره می‌کند، پس کدِ رسیده همان لحظه قابلِ تأیید است؛ ولی + * جوابِ کاوه‌نگار گاهی بعد از رسیدنِ پیامک می‌آید (۱۶ ثانیه در ۲۰۲۶-۰۹-۲۷) و + * منتظر ماندن برایش کاربر را بی‌دلیل معطل می‌کرد. + */ async function requestOtp() { - if (!phoneValid || loading) return; + if (!phoneValid || sending || loading) return; + const mine = ++attempt.current; setError(''); - setLoading('otp'); + setDevOtp(null); + setOtp(''); + setStep('otp'); + setResendIn(RESEND_SECONDS); + setSending(true); try { const data = await api.auth.requestOtp(phone); + if (mine !== attempt.current) return; setDevOtp(data.devOtp ?? null); - setOtp(''); - setStep('otp'); - setResendIn(RESEND_SECONDS); } catch (err) { + if (mine !== attempt.current) return; setError(friendlyError(err, 'ارسالِ کد انجام نشد، دوباره امتحان کن')); + if (err instanceof ApiError && err.status === 400) { + setStep('phone'); // شماره را سرور نپذیرفت؛ برگرد تا اصلاحش کند + } else if (err instanceof ApiError && err.isRateLimited && err.retryAfterMs) { + setResendIn(Math.ceil(err.retryAfterMs / 1000)); + } else { + setResendIn(0); // پیامکی نرفت و سرور cooldown را برداشته؛ همین حالا دوباره بفرستد + } } finally { - setLoading(null); + if (mine === attempt.current) setSending(false); } } @@ -166,16 +187,19 @@ export function LoginScreen({ }`} /> + {sending && !error && ( +

پیامک در راهه؛ همین که رسید کد رو بزن

+ )} {devOtp && (

کدِ آزمایشیِ حالتِ توسعه: {devOtp}

)}
- -
@@ -187,7 +211,7 @@ export function LoginScreen({
{step === 'phone' ? 'دریافتِ کدِ تأیید' : 'ورود'}