ui phase 3

This commit is contained in:
hamid
2026-07-18 01:56:17 +03:30
parent 222856d600
commit 53b4e1b0a4
33 changed files with 1000 additions and 81 deletions
+34 -14
View File
@@ -1,7 +1,7 @@
'use client';
import { FormEvent, FunctionComponent, useEffect, useState } from 'react';
import { CircularProgress, Link as MuiLink, Stack, Typography } from '@mui/material';
import { useTranslations } from 'next-intl';
import { Box, CircularProgress, Link as MuiLink, Stack, Typography } from '@mui/material';
import { useLocale, useTranslations } from 'next-intl';
import OtpInput from '@/components/OtpInput';
import { maskIranMobile } from '@/components/PhoneNumberField';
@@ -9,8 +9,10 @@ import { AppButton } from '@/components';
import { APP_ROLES, type AppRole } from '@/constants';
import { useRequestOtp, useVerifyOtp } from '@/services/auth';
import { OTP_CODE_LENGTH, OTP_LOCKED_CODE } from '@/services/auth/constants';
import { formatClock } from '@/utils';
import type { ApiError } from '@/lib/api/errors';
import { useCountdown } from './useCountdown';
import { useWebOtp } from './useWebOtp';
interface OtpStepProps {
phone: string;
@@ -21,17 +23,11 @@ interface OtpStepProps {
onChangeNumber: () => void;
}
function formatMmSs(totalSeconds: number): string {
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
/**
* A2 (customer) / B2 (nurse) — the OTP code step. Auto-verifies once the last box is filled,
* runs a single resend countdown (cleaned up on unmount), and renders explicit wrong-code,
* expired-code and max-attempts-lockout states. Same mechanism for both actors; the CTA copy
* differs.
* A2 (customer) / B2 (nurse) — the OTP code step. Auto-verifies once the last box is filled
* (manually or via WebOTP autofill), runs a single resend countdown (cleaned up on unmount), and
* renders explicit wrong-code, expired-code and max-attempts-lockout states. Same mechanism for
* both actors; the CTA copy differs.
* @component OtpStep
*/
const OtpStep: FunctionComponent<OtpStepProps> = ({
@@ -42,6 +38,7 @@ const OtpStep: FunctionComponent<OtpStepProps> = ({
onChangeNumber,
}) => {
const t = useTranslations('auth');
const locale = useLocale();
const [code, setCode] = useState('');
const [locked, setLocked] = useState(false);
const countdown = useCountdown();
@@ -68,6 +65,13 @@ const OtpStep: FunctionComponent<OtpStepProps> = ({
);
};
// Reads the SMS code on a supporting browser (REQ-039) and feeds it through the same path
// manual entry uses. Stops (aborts the pending read) once a verification is underway.
useWebOtp((otpCode) => {
setCode(otpCode);
verify(otpCode);
}, !locked && !verifyOtp.isPending);
const submit = (event: FormEvent) => {
event.preventDefault();
verify(code);
@@ -97,7 +101,15 @@ const OtpStep: FunctionComponent<OtpStepProps> = ({
{t('otp_title')}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('otp_sent_to', { phone: maskIranMobile(phone) })}
{t.rich('otp_sent_to', {
// <bdi dir="ltr"> isolates the masked number so the RTL sentence's bidi algorithm
// never reorders the digit/bullet runs around it (the classic digits-around-neutrals bug).
phone: () => (
<Box component="bdi" dir="ltr">
{maskIranMobile(phone)}
</Box>
),
})}
</Typography>
</Stack>
@@ -136,7 +148,15 @@ const OtpStep: FunctionComponent<OtpStepProps> = ({
<Stack sx={{ alignItems: 'center', gap: 1 }}>
{resendDisabled && countdown.isActive && !locked ? (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('resend_in', { time: formatMmSs(countdown.seconds) })}
{t.rich('resend_in', {
// Same bidi-isolation reasoning as the phone echo above — keeps mm:ss reading
// left-to-right (in Persian digits on /fa) inside the RTL sentence.
time: () => (
<Box component="bdi" dir="ltr">
{formatClock(countdown.seconds, locale)}
</Box>
),
})}
</Typography>
) : (
<MuiLink