Merge pull request #43 from rkheftan/fix/otp-mobile

Fix/otp mobile
This commit is contained in:
SajadMRjl
2025-10-11 12:31:50 +03:30
committed by GitHub
7 changed files with 154 additions and 88 deletions

View File

@@ -8,7 +8,10 @@
"emailIsInvalid": "Email is invalid",
"phoneNumberIsInvalid": "Phone number is invalid",
"thisFieldIsRequired": "This field is required",
"googleAuthenticationFailed": "Login with google failed"
"googleAuthenticationFailed": "Login with google failed",
"persian": "Persian(Fa)",
"english": "English(En)",
"accountInfo": "Harmony Account - 2025"
},
"verify": {
"verify": "Verify",

View File

@@ -8,7 +8,10 @@
"emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد",
"phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد",
"thisFieldIsRequired": "این فیلد الزامی است",
"googleAuthenticationFailed": "ورود با گوگل با خطا مواجه شد"
"googleAuthenticationFailed": "ورود با گوگل با خطا مواجه شد",
"persian": "فارسی(Fa)",
"english": "انگلیسی(En)",
"accountInfo": "۱۴۰۴-هارمونی اکانت"
},
"verify": {
"verify": "اعتبارسنجی",

View File

@@ -25,6 +25,10 @@ const DigitInput: React.FC<DigitInputProps> = ({
const { i18n } = useTranslation();
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
const isMobile =
typeof navigator !== 'undefined' &&
/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
useEffect(() => {
inputRefs.current[0]?.focus();
}, []);
@@ -44,7 +48,7 @@ const DigitInput: React.FC<DigitInputProps> = ({
setCode(newCode);
handleDigitInputValueChange(newCode);
if (value && index < 4 - 1) {
if (value && index < newCode.length - 1) {
inputRefs.current[index + 1]?.focus();
}
};
@@ -53,37 +57,33 @@ const DigitInput: React.FC<DigitInputProps> = ({
event: KeyboardEvent<HTMLDivElement>,
index: number,
) => {
if (event.key === 'Backspace' && code[index]) {
event.preventDefault();
if (index >= 0) {
handleChange('', index);
inputRefs.current[index - 1]?.focus();
if (index > 0) inputRefs.current[index - 1]?.focus();
}
};
const handlePaste = (event: React.ClipboardEvent) => {
event.preventDefault();
const pastedData = event.clipboardData.getData('text').replace(/\D/g, ''); // Remove non-digit characters
const pastedData = event.clipboardData.getData('text').replace(/\D/g, '');
const newCode = [...code];
pastedData.split('').forEach((digit, i) => {
if (i < code.length) {
newCode[i] = digit;
}
if (i < newCode.length) newCode[i] = digit;
});
setCode(newCode);
handleDigitInputValueChange(newCode);
// Focus the next empty input after the last pasted character
const lastIndex = Math.min(pastedData.length, code.length) - 1;
if (lastIndex >= 0 && inputRefs.current[lastIndex]) {
inputRefs.current[lastIndex]?.focus();
}
const nextEmpty = newCode.findIndex((d) => d === '');
const focusIndex = nextEmpty === -1 ? newCode.length - 1 : nextEmpty;
inputRefs.current[focusIndex]?.focus();
};
return (
<Stack
direction={i18n.dir() == 'ltr' ? 'row' : 'row-reverse'}
direction={i18n.dir() === 'ltr' ? 'row' : 'row-reverse'}
alignItems="center"
sx={{ gap: 2, width: '100%', my: 4 }}
justifyContent="center"
@@ -97,11 +97,18 @@ const DigitInput: React.FC<DigitInputProps> = ({
autoFocus={index === 0}
value={digit}
onChange={(e) => handleChange(e.target.value, index)}
onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e, index)}
onPaste={(e) => handlePaste(e)}
onKeyDown={(e) => handleBackspace(e, index)}
onPaste={handlePaste}
type={isMobile ? 'tel' : 'text'}
slotProps={{
htmlInput: {
maxLength: 1,
inputMode: isMobile ? 'numeric' : undefined,
pattern: isMobile ? '[0-9]*' : undefined,
autoComplete: 'one-time-code',
name: 'one-time-code',
enterKeyHint: 'done',
dir: 'ltr',
sx: {
height: '72px',
color: error

View File

@@ -0,0 +1,46 @@
import { Box, Typography, MenuItem, Select, Stack } from '@mui/material';
import { Global } from 'iconsax-react';
import { useTranslation } from 'react-i18next';
export default function LanguageAccountBar() {
const { t, i18n } = useTranslation('authentication');
const handleChange = (event: any) => {
const lang = event.target.value;
i18n.changeLanguage(lang);
document.body.dir = lang === 'fa' ? 'rtl' : 'ltr';
};
return (
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
px: 2,
py: 1,
position: 'absolute',
bottom: 0,
direction: i18n.language === 'fa' ? 'rtl' : 'ltr',
}}
>
<Stack direction="row" alignItems="center" spacing={1}>
<Select
size="small"
value={i18n.language}
onChange={handleChange}
variant="standard"
disableUnderline
>
<MenuItem value="fa">{t('loginForm.persian')}</MenuItem>
<MenuItem value="en">{t('loginForm.english')}</MenuItem>
</Select>
<Global size={20} style={{ color: '#666' }} />
</Stack>
<Typography sx={{ color: 'text.primary' }} variant="body2">
{t('loginForm.accountInfo')}
</Typography>
</Box>
);
}

View File

@@ -15,6 +15,7 @@ import { useApi } from '@/hooks/useApi';
import type { GenerateTokenResponse } from '../../api/identityAPI';
import { GoogleAuthenticationV2 } from './GoogleAuthenticationV2';
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
import LanguageAccountBar from './LanguageSwitcher';
export interface LoginRegisterFormProps {
loginRegisterValue: string;
@@ -143,6 +144,7 @@ export function LoginRegisterForm({
const showAdornment = authType === 'phone' && loginRegisterValue.length > 0;
return (
<>
<AuthenticationCard>
<Box
component="form"
@@ -210,5 +212,7 @@ export function LoginRegisterForm({
</Stack>
</Box>
</AuthenticationCard>
<LanguageAccountBar />
</>
);
}

View File

@@ -214,13 +214,9 @@ export function EmailSection(props: EmailSectionProps) {
<TextField
label={t('completion.verificationCode')}
variant="outlined"
type="text"
type="tel"
value={verificationCode}
onChange={handleVerificationCodeChange}
sx={{
flex: '1 1 260px',
'& .MuiOutlinedInput-root': { height: INPUT_H },
}}
disabled={isVerifyingCode}
onBlur={() => handleBlur('verificationCode')}
error={touched.verificationCode && !!errors.verificationCode}
@@ -229,6 +225,13 @@ export function EmailSection(props: EmailSectionProps) {
inputMode: 'numeric',
pattern: '[0-9]*',
maxLength: 4,
autoComplete: 'one-time-code',
name: 'one-time-code',
dir: 'ltr',
}}
sx={{
flex: '1 1 260px',
'& .MuiOutlinedInput-root': { height: 56 },
}}
/>

View File

@@ -48,7 +48,7 @@ export async function saveProfile(payload: {
export async function sendVerificationCode(payload: { phoneNumber: string }) {
return apiClient.post<PhoneNumberApiResponse>(
'/Profile/SendVerfiyPhoneNumberCode',
'/Profile/SendChangePhoneNumberCode',
payload,
);
}