@@ -8,7 +8,10 @@
|
|||||||
"emailIsInvalid": "Email is invalid",
|
"emailIsInvalid": "Email is invalid",
|
||||||
"phoneNumberIsInvalid": "Phone number is invalid",
|
"phoneNumberIsInvalid": "Phone number is invalid",
|
||||||
"thisFieldIsRequired": "This field is required",
|
"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": "Verify",
|
"verify": "Verify",
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
"emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد",
|
"emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد",
|
||||||
"phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد",
|
"phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد",
|
||||||
"thisFieldIsRequired": "این فیلد الزامی است",
|
"thisFieldIsRequired": "این فیلد الزامی است",
|
||||||
"googleAuthenticationFailed": "ورود با گوگل با خطا مواجه شد"
|
"googleAuthenticationFailed": "ورود با گوگل با خطا مواجه شد",
|
||||||
|
"persian": "فارسی(Fa)",
|
||||||
|
"english": "انگلیسی(En)",
|
||||||
|
"accountInfo": "۱۴۰۴-هارمونی اکانت"
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"verify": "اعتبارسنجی",
|
"verify": "اعتبارسنجی",
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ const DigitInput: React.FC<DigitInputProps> = ({
|
|||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
|
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
|
||||||
|
|
||||||
|
const isMobile =
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRefs.current[0]?.focus();
|
inputRefs.current[0]?.focus();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -44,7 +48,7 @@ const DigitInput: React.FC<DigitInputProps> = ({
|
|||||||
setCode(newCode);
|
setCode(newCode);
|
||||||
handleDigitInputValueChange(newCode);
|
handleDigitInputValueChange(newCode);
|
||||||
|
|
||||||
if (value && index < 4 - 1) {
|
if (value && index < newCode.length - 1) {
|
||||||
inputRefs.current[index + 1]?.focus();
|
inputRefs.current[index + 1]?.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -53,37 +57,33 @@ const DigitInput: React.FC<DigitInputProps> = ({
|
|||||||
event: KeyboardEvent<HTMLDivElement>,
|
event: KeyboardEvent<HTMLDivElement>,
|
||||||
index: number,
|
index: number,
|
||||||
) => {
|
) => {
|
||||||
event.preventDefault();
|
if (event.key === 'Backspace' && code[index]) {
|
||||||
if (index >= 0) {
|
event.preventDefault();
|
||||||
handleChange('', index);
|
handleChange('', index);
|
||||||
inputRefs.current[index - 1]?.focus();
|
if (index > 0) inputRefs.current[index - 1]?.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePaste = (event: React.ClipboardEvent) => {
|
const handlePaste = (event: React.ClipboardEvent) => {
|
||||||
event.preventDefault();
|
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];
|
const newCode = [...code];
|
||||||
|
|
||||||
pastedData.split('').forEach((digit, i) => {
|
pastedData.split('').forEach((digit, i) => {
|
||||||
if (i < code.length) {
|
if (i < newCode.length) newCode[i] = digit;
|
||||||
newCode[i] = digit;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setCode(newCode);
|
setCode(newCode);
|
||||||
handleDigitInputValueChange(newCode);
|
handleDigitInputValueChange(newCode);
|
||||||
|
|
||||||
// Focus the next empty input after the last pasted character
|
const nextEmpty = newCode.findIndex((d) => d === '');
|
||||||
const lastIndex = Math.min(pastedData.length, code.length) - 1;
|
const focusIndex = nextEmpty === -1 ? newCode.length - 1 : nextEmpty;
|
||||||
if (lastIndex >= 0 && inputRefs.current[lastIndex]) {
|
inputRefs.current[focusIndex]?.focus();
|
||||||
inputRefs.current[lastIndex]?.focus();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
direction={i18n.dir() == 'ltr' ? 'row' : 'row-reverse'}
|
direction={i18n.dir() === 'ltr' ? 'row' : 'row-reverse'}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
sx={{ gap: 2, width: '100%', my: 4 }}
|
sx={{ gap: 2, width: '100%', my: 4 }}
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
@@ -97,11 +97,18 @@ const DigitInput: React.FC<DigitInputProps> = ({
|
|||||||
autoFocus={index === 0}
|
autoFocus={index === 0}
|
||||||
value={digit}
|
value={digit}
|
||||||
onChange={(e) => handleChange(e.target.value, index)}
|
onChange={(e) => handleChange(e.target.value, index)}
|
||||||
onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e, index)}
|
onKeyDown={(e) => handleBackspace(e, index)}
|
||||||
onPaste={(e) => handlePaste(e)}
|
onPaste={handlePaste}
|
||||||
|
type={isMobile ? 'tel' : 'text'}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: {
|
htmlInput: {
|
||||||
maxLength: 1,
|
maxLength: 1,
|
||||||
|
inputMode: isMobile ? 'numeric' : undefined,
|
||||||
|
pattern: isMobile ? '[0-9]*' : undefined,
|
||||||
|
autoComplete: 'one-time-code',
|
||||||
|
name: 'one-time-code',
|
||||||
|
enterKeyHint: 'done',
|
||||||
|
dir: 'ltr',
|
||||||
sx: {
|
sx: {
|
||||||
height: '72px',
|
height: '72px',
|
||||||
color: error
|
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 type { GenerateTokenResponse } from '../../api/identityAPI';
|
||||||
import { GoogleAuthenticationV2 } from './GoogleAuthenticationV2';
|
import { GoogleAuthenticationV2 } from './GoogleAuthenticationV2';
|
||||||
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||||
|
import LanguageAccountBar from './LanguageSwitcher';
|
||||||
|
|
||||||
export interface LoginRegisterFormProps {
|
export interface LoginRegisterFormProps {
|
||||||
loginRegisterValue: string;
|
loginRegisterValue: string;
|
||||||
@@ -143,72 +144,75 @@ export function LoginRegisterForm({
|
|||||||
const showAdornment = authType === 'phone' && loginRegisterValue.length > 0;
|
const showAdornment = authType === 'phone' && loginRegisterValue.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthenticationCard>
|
<>
|
||||||
<Box
|
<AuthenticationCard>
|
||||||
component="form"
|
<Box
|
||||||
onSubmit={(e) => {
|
component="form"
|
||||||
e.preventDefault();
|
onSubmit={(e) => {
|
||||||
e.stopPropagation();
|
e.preventDefault();
|
||||||
if (!userStatusLoading) {
|
e.stopPropagation();
|
||||||
void handleSubmit();
|
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}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
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}>
|
<TextField
|
||||||
<Button loading={userStatusLoading} type="submit">
|
ref={textFieldRef}
|
||||||
{t('loginForm.submitButton')}
|
inputRef={inputRef}
|
||||||
</Button>
|
label={t('loginForm.emailOrPhoneLabel')}
|
||||||
|
value={loginRegisterValue}
|
||||||
<GoogleAuthenticationV2
|
onChange={handleInputChange}
|
||||||
authFactory={authFactory}
|
onBlur={handleBlur}
|
||||||
onGoogleAuthenticated={onGoogleAuthenticated}
|
error={inputError}
|
||||||
disabled={userStatusLoading}
|
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>
|
<Stack spacing={2}>
|
||||||
</AuthenticationCard>
|
<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
|
<TextField
|
||||||
label={t('completion.verificationCode')}
|
label={t('completion.verificationCode')}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="text"
|
type="tel"
|
||||||
value={verificationCode}
|
value={verificationCode}
|
||||||
onChange={handleVerificationCodeChange}
|
onChange={handleVerificationCodeChange}
|
||||||
sx={{
|
|
||||||
flex: '1 1 260px',
|
|
||||||
'& .MuiOutlinedInput-root': { height: INPUT_H },
|
|
||||||
}}
|
|
||||||
disabled={isVerifyingCode}
|
disabled={isVerifyingCode}
|
||||||
onBlur={() => handleBlur('verificationCode')}
|
onBlur={() => handleBlur('verificationCode')}
|
||||||
error={touched.verificationCode && !!errors.verificationCode}
|
error={touched.verificationCode && !!errors.verificationCode}
|
||||||
@@ -229,6 +225,13 @@ export function EmailSection(props: EmailSectionProps) {
|
|||||||
inputMode: 'numeric',
|
inputMode: 'numeric',
|
||||||
pattern: '[0-9]*',
|
pattern: '[0-9]*',
|
||||||
maxLength: 4,
|
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 }) {
|
export async function sendVerificationCode(payload: { phoneNumber: string }) {
|
||||||
return apiClient.post<PhoneNumberApiResponse>(
|
return apiClient.post<PhoneNumberApiResponse>(
|
||||||
'/Profile/SendVerfiyPhoneNumberCode',
|
'/Profile/SendChangePhoneNumberCode',
|
||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user