@@ -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",
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
"emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد",
|
||||
"phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد",
|
||||
"thisFieldIsRequired": "این فیلد الزامی است",
|
||||
"googleAuthenticationFailed": "ورود با گوگل با خطا مواجه شد"
|
||||
"googleAuthenticationFailed": "ورود با گوگل با خطا مواجه شد",
|
||||
"persian": "فارسی(Fa)",
|
||||
"english": "انگلیسی(En)",
|
||||
"accountInfo": "۱۴۰۴-هارمونی اکانت"
|
||||
},
|
||||
"verify": {
|
||||
"verify": "اعتبارسنجی",
|
||||
|
||||
@@ -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,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
if (index >= 0) {
|
||||
if (event.key === 'Backspace' && code[index]) {
|
||||
event.preventDefault();
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,72 +144,75 @@ export function LoginRegisterForm({
|
||||
const showAdornment = authType === 'phone' && loginRegisterValue.length > 0;
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!userStatusLoading) {
|
||||
void handleSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h5">{t('loginForm.title')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('loginForm.description')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
label={t('loginForm.emailOrPhoneLabel')}
|
||||
value={loginRegisterValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
||||
input: {
|
||||
endAdornment: i18n.dir() === 'rtl' && (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={showAdornment}
|
||||
menuAnchor={menuAnchorEl}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
startAdornment: i18n.dir() === 'ltr' && (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={showAdornment}
|
||||
menuAnchor={menuAnchorEl}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
<>
|
||||
<AuthenticationCard>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!userStatusLoading) {
|
||||
void handleSubmit();
|
||||
}
|
||||
}}
|
||||
sx={{ my: 4 }}
|
||||
/>
|
||||
>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h5">{t('loginForm.title')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('loginForm.description')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Button loading={userStatusLoading} type="submit">
|
||||
{t('loginForm.submitButton')}
|
||||
</Button>
|
||||
|
||||
<GoogleAuthenticationV2
|
||||
authFactory={authFactory}
|
||||
onGoogleAuthenticated={onGoogleAuthenticated}
|
||||
disabled={userStatusLoading}
|
||||
<TextField
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
label={t('loginForm.emailOrPhoneLabel')}
|
||||
value={loginRegisterValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
||||
input: {
|
||||
endAdornment: i18n.dir() === 'rtl' && (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={showAdornment}
|
||||
menuAnchor={menuAnchorEl}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
startAdornment: i18n.dir() === 'ltr' && (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={showAdornment}
|
||||
menuAnchor={menuAnchorEl}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ my: 4 }}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</AuthenticationCard>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Button loading={userStatusLoading} type="submit">
|
||||
{t('loginForm.submitButton')}
|
||||
</Button>
|
||||
|
||||
<GoogleAuthenticationV2
|
||||
authFactory={authFactory}
|
||||
onGoogleAuthenticated={onGoogleAuthenticated}
|
||||
disabled={userStatusLoading}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</AuthenticationCard>
|
||||
<LanguageAccountBar />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user