ui phase 3
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user