import { useEffect, useRef, useState } from 'react'; import { ChevronRight, Phone } from 'lucide-react'; import { api, ApiError } from '@/api'; import { InlineError, PrimaryButton } from '@/components/ui'; import { SeyarMascot } from '@/mascots/SeyarMascot'; import { friendlyError } from '@/lib/errors'; import { digitsOnly, toFa } from '@/lib/format'; const OTP_LENGTH = 6; /** همان cooldownِ سرور (OTP_REQUEST_PHONE_COOLDOWN_SECONDS). */ const RESEND_SECONDS = 120; /** * ورود با شماره (OTP): تنها راهِ ورود است. */ export function LoginScreen({ onDone, onCancel }: { onDone: () => void; onCancel?: () => void }) { const [step, setStep] = useState<'phone' | 'otp'>('phone'); const [phone, setPhone] = useState(''); const [otp, setOtp] = useState(''); const [devOtp, setDevOtp] = useState(null); const [loading, setLoading] = useState<'login' | 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); // کدِ سرور ۶ رقمی است و هر حدسِ غلط یکی از ۵ فرصت را می‌سوزاند؛ کدِ ناقص نباید فرستاده شود. // فقط در حالتِ توسعه کدِ ثابتِ ۴ رقمیِ DEV_MASTER_OTP هم هست. const otpValid = otp.length === OTP_LENGTH || (import.meta.env.DEV && otp.length === 4); useEffect(() => { if (resendIn <= 0) return; const t = setTimeout(() => setResendIn((s) => s - 1), 1000); return () => clearTimeout(t); }, [resendIn]); useEffect(() => { if (step === 'otp') otpInput.current?.focus(); }, [step]); /** * صفحه‌ی کد فوراً باز می‌شود و درخواست در پس‌زمینه جلو می‌رود. سرور کد را پیش از * تماس با کاوه‌نگار ذخیره می‌کند، پس کدِ رسیده همان لحظه قابلِ تأیید است؛ ولی * جوابِ کاوه‌نگار گاهی بعد از رسیدنِ پیامک می‌آید (۱۶ ثانیه در ۲۰۲۶-۰۹-۲۷) و * منتظر ماندن برایش کاربر را بی‌دلیل معطل می‌کرد. */ async function requestOtp() { if (!phoneValid || sending || loading) return; const mine = ++attempt.current; setError(''); 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); } 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 { if (mine === attempt.current) setSending(false); } } async function verify(code = otp) { if (!(code.length === OTP_LENGTH || (import.meta.env.DEV && code.length === 4)) || loading) return; setError(''); setLoading('login'); try { await api.auth.login(phone, code); onDone(); } catch (err) { setError(friendlyError(err, 'کد درست نبود، دوباره امتحان کن')); setOtp(''); } finally { setLoading(null); } } function onOtpChange(value: string) { const code = digitsOnly(value, OTP_LENGTH); setOtp(code); if (code.length === OTP_LENGTH) void verify(code); } return (
{onCancel && ( )}

ورود به تدریسینو ریاضی

{step === 'phone' ? 'شماره موبایلت رو وارد کن' : `کد ${toFa(OTP_LENGTH)} رقمی پیامک‌شده رو وارد کن`}

{ e.preventDefault(); void (step === 'phone' ? requestOtp() : verify()); }} > {step === 'phone' ? ( ) : (
{sending && !error && (

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

)} {devOtp && (

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

)}
)} {error && {error}}
{step === 'phone' ? 'دریافت کد تأیید' : 'ورود'}
); } /** * پرکردنِ خودکارِ گوشی و چسباندن معمولاً «+98 912 …» یا «0098…» می‌دهد؛ قبلاً فقط ۱۱ رقمِ اول نگه داشته می‌شد * («98912…») و دکمه بی‌توضیح خاموش می‌ماند. سرور هم همین شکل‌ها را می‌پذیرد (accounts/phone.py). */ function normalizePhoneInput(value: string): string { let digits = digitsOnly(value); if (digits.startsWith('0098')) digits = `0${digits.slice(4)}`; else if (digits.startsWith('98') && digits.length >= 12) digits = `0${digits.slice(2)}`; // «98…» هنوز در حالِ تایپ است (۱۰ و ۱۱ رقمِ وسطِ «+98912…» هم با ۹ شروع می‌شوند)؛ پیش‌شماره‌ی ۰۹۸ نداریم. else if (digits.startsWith('9') && !digits.startsWith('98') && digits.length === 10) digits = `0${digits}`; return digits.slice(0, 11); }