Merge branch 'develop' into feat/google-auth
This commit is contained in:
@@ -99,7 +99,7 @@ export const CompleteSignUp = ({
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
|
||||
@@ -54,6 +54,7 @@ export function LoginRegisterForm({
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let newValue = event.target.value;
|
||||
newValue = replacePersianWithRealNumbers(newValue);
|
||||
if (newValue.startsWith('09')) {
|
||||
newValue = newValue.substring(1);
|
||||
}
|
||||
@@ -152,7 +153,7 @@ export function LoginRegisterForm({
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
|
||||
@@ -56,6 +56,12 @@ export function OtpVerifyForm({
|
||||
const { loading: loginSignUpLoading, execute: loginSignUpCall } =
|
||||
useApi(loginOrSignUpWithOtp);
|
||||
|
||||
useEffect(() => {
|
||||
if (otpCode.length === 4) {
|
||||
handleVerifyOTP();
|
||||
}
|
||||
}, [otpCode]);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (resendTimer > 0) {
|
||||
|
||||
@@ -35,6 +35,12 @@ export function VerifyPhoneNumber({
|
||||
const { loading: confirmSmsOtpLoading, execute: confirmSmsOtpCall } =
|
||||
useApi(confirmSmsOtp);
|
||||
|
||||
useEffect(() => {
|
||||
if (otpCode.length === 4) {
|
||||
handleVerifyOTP();
|
||||
}
|
||||
}, [otpCode]);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (resendTimer > 0) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { countries, type Country } from '../../../data/countries';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { LTRBox } from '@/components/common/LTRBox';
|
||||
interface CountryCodeSelectorProps {
|
||||
show: boolean;
|
||||
value: CountryCode;
|
||||
@@ -187,7 +188,9 @@ export function CountryCodeSelector({
|
||||
{filteredCountries.length === 0 ? (
|
||||
<MenuItem disabled>
|
||||
<ListItem>
|
||||
<ListItemText>{t('messages.noResualtFound')}</ListItemText>
|
||||
<ListItemText>
|
||||
{t('messages.noResualtFound', { ns: 'common' })}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
</MenuItem>
|
||||
) : (
|
||||
@@ -209,7 +212,9 @@ export function CountryCodeSelector({
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t(country.label, { ns: 'country' })} />
|
||||
<Typography color="text.secondary">
|
||||
{country.phone}
|
||||
<Typography>
|
||||
<LTRBox>{country.phone}</LTRBox>
|
||||
</Typography>
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))
|
||||
|
||||
@@ -48,6 +48,12 @@ export function ForgetPasswordOtp({
|
||||
execute: confirmForgetPassCodeCall,
|
||||
} = useApi(confirmForgetPassCode);
|
||||
|
||||
useEffect(() => {
|
||||
if (otpCode.length === 4) {
|
||||
handleVerifyOTP();
|
||||
}
|
||||
}, [otpCode]);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (resendTimer > 0) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { SendForgetPassCodeRequest } from '../../types/userTypes';
|
||||
import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
|
||||
import { useToast } from '@rkheftan/harmony-ui';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||
|
||||
export interface ForgettedPasswordInfoProps {
|
||||
forgettedPasswordInfo: string;
|
||||
@@ -46,6 +47,7 @@ export function ForgettedPasswordInfo({
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let newValue = event.target.value;
|
||||
newValue = replacePersianWithRealNumbers(newValue);
|
||||
if (newValue.startsWith('09')) {
|
||||
newValue = newValue.substring(1);
|
||||
}
|
||||
@@ -138,7 +140,7 @@ export function ForgettedPasswordInfo({
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
DatePicker,
|
||||
PickersDay,
|
||||
@@ -13,7 +13,6 @@ import { getDay } from 'date-fns-jalali';
|
||||
import { format as formatJalali } from 'date-fns-jalali';
|
||||
import { format } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
import { type DateOfBirthProps } from '../../types/settingForm';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { Calendar } from 'iconsax-react';
|
||||
@@ -21,6 +20,7 @@ import { Calendar } from 'iconsax-react';
|
||||
export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
const isFarsi = i18n.language === 'fa' || i18n.language === 'fa-IR';
|
||||
const [openView, setOpenView] = useState<'year' | 'month' | 'day'>('year');
|
||||
|
||||
const { Adapter, locale, formatString, dayOfWeekFormatter } = useMemo(() => {
|
||||
if (isFarsi) {
|
||||
@@ -44,11 +44,7 @@ export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
const dayNumber = isFarsi
|
||||
? formatJalali(props.day, 'dd')
|
||||
: format(props.day, 'dd');
|
||||
return (
|
||||
<PickersDay {...props}>
|
||||
{toLocaleDigits(dayNumber, i18n.language)}
|
||||
</PickersDay>
|
||||
);
|
||||
return <PickersDay {...props}>{dayNumber}</PickersDay>;
|
||||
};
|
||||
|
||||
const CustomCalendarIcon = () => (
|
||||
@@ -58,23 +54,23 @@ export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={Adapter} adapterLocale={locale}>
|
||||
<DatePicker
|
||||
label={toLocaleDigits(t('completion.dateOfBirth'), i18n.language)}
|
||||
label={t('completion.dateOfBirth')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
format={formatString}
|
||||
dayOfWeekFormatter={dayOfWeekFormatter}
|
||||
slots={{ day: CustomDay, openPickerIcon: CustomCalendarIcon }}
|
||||
disableFuture
|
||||
openTo="year"
|
||||
views={['year', 'month', 'day']}
|
||||
view={openView}
|
||||
onViewChange={(newView) => setOpenView(newView)}
|
||||
onYearChange={() => setOpenView('month')}
|
||||
slotProps={{
|
||||
textField: {
|
||||
fullWidth: true,
|
||||
},
|
||||
textField: { fullWidth: true },
|
||||
popper: {
|
||||
sx: {
|
||||
'& .MuiDateCalendar-root': {
|
||||
// TODO: fix this to use textfield width instead of defining hardcode
|
||||
width: '309px',
|
||||
},
|
||||
'& .MuiDateCalendar-root': { width: '309px' },
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
TextField,
|
||||
Box,
|
||||
@@ -13,30 +13,35 @@ import { useTranslation } from 'react-i18next';
|
||||
import { TickCircle, Edit2 } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type EmailSectionProps } from '../../types/settingForm';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
import { sanitizeLocalNumber } from '@/utils/regexes/sanitizeNumber';
|
||||
|
||||
export function EmailSection({
|
||||
showEmail,
|
||||
setShowEmail,
|
||||
email,
|
||||
setEmail,
|
||||
codeSent,
|
||||
verificationCode,
|
||||
setVerificationCode,
|
||||
buttonState,
|
||||
handleSendCode,
|
||||
handleVerifyCode,
|
||||
emailVerified,
|
||||
isVerifyingCode,
|
||||
isSendingCode,
|
||||
handleEditEmail,
|
||||
errors,
|
||||
touched,
|
||||
handleBlur,
|
||||
countdown,
|
||||
}: EmailSectionProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
const ADORN_W = 24;
|
||||
const INPUT_H = 56;
|
||||
|
||||
export function EmailSection(props: EmailSectionProps) {
|
||||
const {
|
||||
showEmail,
|
||||
setShowEmail,
|
||||
email,
|
||||
setEmail,
|
||||
codeSent,
|
||||
verificationCode,
|
||||
setVerificationCode,
|
||||
buttonState,
|
||||
handleSendCode,
|
||||
handleVerifyCode,
|
||||
emailVerified,
|
||||
isVerifyingCode,
|
||||
isSendingCode,
|
||||
handleEditEmail,
|
||||
errors,
|
||||
touched,
|
||||
handleBlur,
|
||||
countdown,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('completionForm');
|
||||
|
||||
const handleToggleEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShowEmail(e.target.checked);
|
||||
@@ -45,29 +50,31 @@ export function EmailSection({
|
||||
const handleVerificationCodeChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const value = e.target.value;
|
||||
// Allow only digits and enforce the max length
|
||||
if (isNumeric(value) && value.length <= 4) {
|
||||
setVerificationCode(value);
|
||||
const normalized = sanitizeLocalNumber(e.target.value);
|
||||
if (isNumeric(normalized) && normalized.length <= 4) {
|
||||
setVerificationCode(normalized);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTimerValue = () => {
|
||||
const m = String(Math.floor(countdown / 60)).padStart(2, '0');
|
||||
const s = String(countdown % 60).padStart(2, '0');
|
||||
return toLocaleDigits(`${m}:${s}`, i18n.language);
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (buttonState !== 'counting') {
|
||||
setVerificationCode('');
|
||||
}
|
||||
}, [buttonState, setVerificationCode]);
|
||||
|
||||
const showCodeSection =
|
||||
showEmail && codeSent && !emailVerified && buttonState === 'counting';
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Switch checked={showEmail} onChange={handleToggleEmail} />
|
||||
<Typography
|
||||
sx={{ color: showEmail ? 'primary.main' : 'text.primary' }}
|
||||
@@ -76,14 +83,9 @@ export function EmailSection({
|
||||
</Typography>
|
||||
</Box>
|
||||
</FormGroup>
|
||||
|
||||
{showEmail && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -97,40 +99,75 @@ export function EmailSection({
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={email}
|
||||
autoFocus
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onBlur={() => handleBlur('email')}
|
||||
disabled={isSendingCode || codeSent}
|
||||
error={touched.email && !!errors.email}
|
||||
helperText={touched.email && errors.email}
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
sx={{
|
||||
flex: '1 1 260px',
|
||||
'& .MuiOutlinedInput-root': { height: INPUT_H },
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment:
|
||||
!isVerifyingCode && emailVerified ? (
|
||||
<InputAdornment position="start">
|
||||
<Icon
|
||||
Component={TickCircle}
|
||||
size="medium"
|
||||
variant="Bold"
|
||||
color="success.main"
|
||||
/>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
endAdornment: codeSent ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleEditEmail}>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
size="medium"
|
||||
/>
|
||||
</IconButton>
|
||||
startAdornment: (
|
||||
<InputAdornment position="start" sx={{ width: ADORN_W }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: ADORN_W,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
visibility:
|
||||
!isVerifyingCode && emailVerified
|
||||
? 'visible'
|
||||
: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
Component={TickCircle}
|
||||
size="medium"
|
||||
variant="Bold"
|
||||
color="success.main"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end" sx={{ width: ADORN_W }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: ADORN_W,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ visibility: codeSent ? 'visible' : 'hidden' }}
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleEditEmail}
|
||||
disabled={!codeSent}
|
||||
>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
size="medium"
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isVerifyingCode &&
|
||||
!emailVerified &&
|
||||
(buttonState === 'counting' ? (
|
||||
@@ -140,6 +177,8 @@ export function EmailSection({
|
||||
width: { xs: '100%', sm: '156px' },
|
||||
alignSelf: 'center',
|
||||
textAlign: 'center',
|
||||
lineHeight: `${INPUT_H}px`,
|
||||
height: INPUT_H,
|
||||
}}
|
||||
color="primary"
|
||||
variant="body1"
|
||||
@@ -154,41 +193,53 @@ export function EmailSection({
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '156px' },
|
||||
alignSelf: 'center',
|
||||
height: INPUT_H,
|
||||
}}
|
||||
>
|
||||
{t('completion.vericationCodeButton')}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
{!emailVerified && codeSent && (
|
||||
|
||||
{showCodeSection && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label={t('completion.verificationCode')}
|
||||
autoFocus
|
||||
variant="outlined"
|
||||
type="text"
|
||||
value={verificationCode}
|
||||
onChange={handleVerificationCodeChange}
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
sx={{
|
||||
flex: '1 1 260px',
|
||||
'& .MuiOutlinedInput-root': { height: INPUT_H },
|
||||
}}
|
||||
disabled={isVerifyingCode}
|
||||
onBlur={() => handleBlur('verificationCode')}
|
||||
error={touched.verificationCode && !!errors.verificationCode}
|
||||
helperText={touched.verificationCode && errors.verificationCode}
|
||||
inputProps={{
|
||||
inputMode: 'numeric',
|
||||
pattern: '[0-9]*',
|
||||
maxLength: 4,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleVerifyCode}
|
||||
loading={isVerifyingCode}
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '156px' },
|
||||
alignSelf: 'center',
|
||||
alignSelf: 'stretch',
|
||||
height: INPUT_H,
|
||||
}}
|
||||
>
|
||||
{t('completion.checkCodeButton')}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type PersonalInfoFieldsProps } from '../../types/settingForm';
|
||||
import { Gender } from '../../types/settingForm';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { sanitizeLocalNumber } from '@/utils/regexes/sanitizeNumber';
|
||||
|
||||
export function PersonalInfoFields({
|
||||
firstName,
|
||||
@@ -129,6 +130,7 @@ export function PersonalInfoFields({
|
||||
value={currentCountry}
|
||||
onChange={(_, newValue) => setCountry(newValue?.code || '')}
|
||||
onBlur={() => handleBlur('country')}
|
||||
autoFocus
|
||||
renderOption={(props, option) => (
|
||||
<Box component="li" {...props} key={option.code}>
|
||||
<ReactCountryFlag
|
||||
@@ -163,7 +165,13 @@ export function PersonalInfoFields({
|
||||
label={t('completion.optionalNationalCode')}
|
||||
placeholder={t('completion.optionalNationalCode')}
|
||||
value={nationalId}
|
||||
onChange={(e) => setNationalId(e.target.value)}
|
||||
onChange={(e) => {
|
||||
const normalized = sanitizeLocalNumber(e.target.value);
|
||||
|
||||
if (normalized.length <= 10) {
|
||||
setNationalId(normalized);
|
||||
}
|
||||
}}
|
||||
variant="outlined"
|
||||
onBlur={() => handleBlur('nationalId')}
|
||||
error={touched.nationalId && !!errors.nationalId}
|
||||
|
||||
@@ -7,9 +7,12 @@ import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { type SubmitProps } from '../../types/settingForm';
|
||||
import { CloseCircle } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
|
||||
export function SubmitSection({ loading }: SubmitProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
@@ -21,6 +24,7 @@ export function SubmitSection({ loading }: SubmitProps) {
|
||||
};
|
||||
|
||||
const agreementText = t('completion.agreement');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
@@ -72,7 +76,16 @@ export function SubmitSection({ loading }: SubmitProps) {
|
||||
maxWidth="md"
|
||||
dir={i18n.language.startsWith('fa') ? 'rtl' : 'ltr'}
|
||||
>
|
||||
<DialogTitle>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={() => setOpenDialog(false)}>
|
||||
<Icon Component={CloseCircle} size="medium" color="primary.main" />
|
||||
</IconButton>
|
||||
{t('completion.rules') || t('completion.rules')}
|
||||
</DialogTitle>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user