diff --git a/src/components/auth/customer-login-form.tsx b/src/components/auth/customer-login-form.tsx index eed4096..4ce57ab 100644 --- a/src/components/auth/customer-login-form.tsx +++ b/src/components/auth/customer-login-form.tsx @@ -25,6 +25,7 @@ export default function CustomerLoginForm({ initialStep = 'mobile' }: { initialS const [step, setStep] = useState(() => initialStep === 'verify' && normalizePhone(readLoginMobile()) ? 'verify' : 'mobile') const [code, setCode] = useState('') const [error, setError] = useState('') + const [notice, setNotice] = useState('') const [busy, setBusy] = useState(false) const [options, setOptions] = useState([]) const [recoveryMobile, setRecoveryMobile] = useState('') @@ -74,6 +75,7 @@ export default function CustomerLoginForm({ initialStep = 'mobile' }: { initialS if (!normalized) { setError('شماره موبایل معتبر وارد کنید.'); return } await run(async () => { setRecoveryMobile('') + setNotice('') let response try { response = await axios.post(process.env.NEXT_PUBLIC_API_URL + '/auth/login/mobile/', { mobile: normalized }, { timeout: 30000 }) @@ -91,6 +93,9 @@ export default function CustomerLoginForm({ initialStep = 'mobile' }: { initialS setCode('') setOptions([]) setStep('verify') // Never depend on storage or route navigation after SMS. + if (response.data?.result?.delivery_status === 'unconfirmed') { + setNotice('وضعیت ارسال پیامک تأیید نشد. اگر کد را دریافت کرده‌اید، آن را وارد کنید؛ در غیر این صورت پس از پایان شمارش دوباره درخواست دهید.') + } const wait = Number(response.data?.result?.retry_after || 60) setNow(Date.now()) setResendAt(Date.now() + (Number.isFinite(wait) ? Math.min(Math.max(wait, 1), 3600) : 60) * 1000) @@ -125,6 +130,7 @@ export default function CustomerLoginForm({ initialStep = 'mobile' }: { initialS setCode('') setOptions([]) setError('') + setNotice('') } return
@@ -153,6 +159,7 @@ export default function CustomerLoginForm({ initialStep = 'mobile' }: { initialS } {remaining > 0 &&

ارسال دوبارهٔ کد پس از {remaining} ثانیه امکان‌پذیر است.

} + {notice &&

{notice}

} {error &&

{error}

}
diff --git a/tests/auth-session.test.cjs b/tests/auth-session.test.cjs index 4fe7fe5..2fcfd78 100644 --- a/tests/auth-session.test.cjs +++ b/tests/auth-session.test.cjs @@ -224,6 +224,36 @@ function customerForm(h, props = {}) { const customerOption = name => ({ uuid: name, cuser_role: { sys_ws: { name }, role_type: { role_name_en: 'workspace_customer' } } }); const settle = () => new Promise(resolve => setImmediate(resolve)); + test('unconfirmed SMS reply opens OTP and the received code completes login', async () => { + const h = harness(); const form = customerForm(h); let sends = 0; + h.axios.defaults.adapter = config => { + if (config.url.endsWith('/auth/login/mobile/')) { + sends++; + return Promise.resolve(response(config, { result: { mobile: 9123456789, retry_after: 60, delivery_status: 'unconfirmed' } }, 202)); + } + if (config.url.endsWith('/auth/login/mobile/verify/')) { + assert.equal(JSON.parse(config.data).opt, '123456'); + return Promise.resolve(response(config, [customerOption('customer-panel')])); + } + return Promise.resolve(response(config, { access: 'access.customer', refresh: 'refresh.customer' })); + }; + form.input('09123456789'); await form.submit(); + assert.equal(elements(form.render(), item => item.props?.autoComplete === 'one-time-code').length, 1); + assert.ok(textContent(form.render()).includes('وضعیت ارسال پیامک تأیید نشد')); + assert.equal(elements(form.render(), item => item.props?.role === 'alert').length, 0); + form.input('123456'); await form.submit(); + assert.equal(sends, 1); assert.equal(h.auth.getAccessToken(), 'access.customer'); + assert.deepEqual(h.redirects, ['/shop']); + }); + test('explicit SMS rejection retains the real error and does not open OTP', async () => { + const h = harness(); const form = customerForm(h); + h.axios.defaults.adapter = config => Promise.reject(failure(config, 503, { detail: 'ارسال پیامک انجام نشد. لطفاً کمی بعد دوباره تلاش کنید.' })); + form.input('09123456789'); await form.submit(); + assert.ok(textContent(form.render()).includes('ارسال پیامک انجام نشد')); + assert.equal(elements(form.render(), item => item.props?.autoComplete === 'one-time-code').length, 0); + assert.equal(h.auth.getAccessToken(), null); + }); + test('SMS success displays OTP in the same page even when sessionStorage is blocked', async () => { const h = harness(); const form = customerForm(h); h.window.sessionStorage.setItem = () => { throw new Error('Storage denied'); };