@@ -2,7 +2,7 @@
|
||||
"loginForm": {
|
||||
"title": "Login/Register",
|
||||
"description": "Please enter your email/password to start",
|
||||
"emailOrPhoneLabel": "Email/Password",
|
||||
"emailOrPhoneLabel": "Email/Phone number",
|
||||
"submitButton": "Login/Register",
|
||||
"loginWithGoogle": "Login with google",
|
||||
"emailIsInvalid": "Email is invalid",
|
||||
@@ -23,6 +23,7 @@
|
||||
"moreMinute": "minute",
|
||||
"resendCode": "Resend code",
|
||||
"confirmAndLogin": "Confirm & login",
|
||||
"confirmAndContinue": "Confirm & continue",
|
||||
"loginWithPassword": "Login with password"
|
||||
},
|
||||
"completeSignUp": {
|
||||
|
||||
@@ -102,8 +102,7 @@
|
||||
"dark": "Dark",
|
||||
"language": "زبان/language",
|
||||
"calendar": "Calendar and date format",
|
||||
"solar": "Solar",
|
||||
"lunar": "Lunar",
|
||||
"jalali": "jalali",
|
||||
"christian": "Christian",
|
||||
"iran": "Iran",
|
||||
"saving": "Saving...",
|
||||
|
||||
@@ -102,8 +102,7 @@
|
||||
"dark": "تاریک",
|
||||
"language": "زبان/language",
|
||||
"calendar": "فرمت تقویم و تاریخ",
|
||||
"solar": "شمسی",
|
||||
"lunar": "قمری",
|
||||
"jalali": "شمسی",
|
||||
"christian": "میلادی",
|
||||
"iran": "ایران",
|
||||
"saving": "در حال ذخیرهسازی...",
|
||||
|
||||
@@ -8,6 +8,7 @@ import React, {
|
||||
} from 'react';
|
||||
import { TextField, Stack } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||
|
||||
interface DigitInputProps {
|
||||
error: boolean;
|
||||
@@ -34,6 +35,8 @@ const DigitInput: React.FC<DigitInputProps> = ({
|
||||
};
|
||||
|
||||
const handleChange = (value: string, index: number) => {
|
||||
value = replacePersianWithRealNumbers(value);
|
||||
|
||||
if (!/^\d$/.test(value) && value !== '') return;
|
||||
|
||||
const newCode = [...code];
|
||||
@@ -91,6 +94,7 @@ const DigitInput: React.FC<DigitInputProps> = ({
|
||||
color={success ? 'success' : 'primary'}
|
||||
key={index}
|
||||
inputRef={(el) => (inputRefs.current[index] = el)}
|
||||
autoFocus={index === 0}
|
||||
value={digit}
|
||||
onChange={(e) => handleChange(e.target.value, index)}
|
||||
onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e, index)}
|
||||
@@ -115,7 +119,7 @@ const DigitInput: React.FC<DigitInputProps> = ({
|
||||
variant="standard"
|
||||
size="medium"
|
||||
sx={{
|
||||
width: '83px',
|
||||
width: { md: '83px', xs: '20%' },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
@@ -9,23 +10,19 @@ import {
|
||||
} from '@mui/material';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { Logout, More } from 'iconsax-react';
|
||||
import type { UserInfo } from '@/contexts/AuthContext';
|
||||
import { LTRTypography } from '../common/LTRTypography';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { FlexBox } from '../common/FlexBox';
|
||||
|
||||
interface HeaderProps {
|
||||
user: UserInfo;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ user }) => {
|
||||
export const Header: React.FC = () => {
|
||||
const { t, i18n } = useTranslation('sideNav');
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const { logout } = useAuth();
|
||||
const { logout, userInfo: user } = useAuth();
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
@@ -52,15 +49,26 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
|
||||
height: (t) => t.spacing(10.5),
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
{user.fullName}
|
||||
</Typography>
|
||||
{/* TODO: add ternary text color to palette */}
|
||||
<LTRTypography variant="body2" color="#757575">
|
||||
{user.phoneNumber ?? user.email}
|
||||
</LTRTypography>
|
||||
</Box>
|
||||
<FlexBox sx={{ alignItems: 'center', gap: 1 }}>
|
||||
<Avatar
|
||||
sx={{ width: 32, height: 32, fontSize: '14px' }}
|
||||
src={
|
||||
user.picture &&
|
||||
import.meta.env.VITE_IMAGE_BASE_URL + '/' + user.picture
|
||||
}
|
||||
>
|
||||
{user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
{user.fullName}
|
||||
</Typography>
|
||||
{/* TODO: add ternary text color to palette */}
|
||||
<LTRTypography variant="body2" color="#757575">
|
||||
{user.phoneNumber ?? user.email}
|
||||
</LTRTypography>
|
||||
</Box>
|
||||
</FlexBox>
|
||||
|
||||
<IconButton onClick={handleClick} color="primary">
|
||||
<Icon Component={More} />
|
||||
|
||||
@@ -4,9 +4,10 @@ import { appRoutes } from '@/routes/config';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { Header } from './Header';
|
||||
import { useState } from 'react';
|
||||
import { Suspense, useState } from 'react';
|
||||
import { Toolbar } from './Toolbar';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { Loading } from '../routes/Loading';
|
||||
|
||||
export const Layout = () => {
|
||||
const navItemConfigs = buildNavItems(appRoutes);
|
||||
@@ -53,7 +54,9 @@ export const Layout = () => {
|
||||
overflowInline: 'auto',
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -63,21 +63,9 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
)}
|
||||
<Logo />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{ display: 'flex', height: '100%', alignItems: 'center', gap: 1 }}
|
||||
>
|
||||
{isMobile && (
|
||||
<Avatar
|
||||
sx={{ width: 32, height: 32, fontSize: '14px' }}
|
||||
src={user.picture}
|
||||
>
|
||||
{user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)}
|
||||
</Avatar>
|
||||
)}
|
||||
<IconButton color="primary" onClick={handleClick}>
|
||||
<Icon Component={Menu} variant="Bold" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<ProductsMenu anchorEl={anchorEl} onClose={handleClose} open={open} />
|
||||
</MuiToolbar>
|
||||
);
|
||||
|
||||
@@ -95,9 +95,10 @@ export const AuthenticationSteps = (): JSX.Element => {
|
||||
tokenResponse: GenerateTokenResponse,
|
||||
) => {
|
||||
setMemoryTokenRes(tokenResponse);
|
||||
if (authFactory.isCurrentApplication()) {
|
||||
login(tokenResponse);
|
||||
}
|
||||
// TODO: For now both application scopes can have their tokens
|
||||
// later we need to discuss this base on business plan and change it
|
||||
// if (authFactory.isCurrentApplication()) {}
|
||||
login(tokenResponse);
|
||||
|
||||
if (loginResult.registeredWithOutPhoneNumber) {
|
||||
setCurrentStep('addPhoneNumber');
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Box, Button, TextField, Typography } from '@mui/material';
|
||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||
import { useRef, useState, type Dispatch } from 'react';
|
||||
import { useRef, useState, type ChangeEvent, type Dispatch } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import { CountryCodeSelector } from '../../../../components/CountryCodeSelector';
|
||||
import { sendSmsOtp } from '../../api/authorizationAPI';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||
|
||||
export interface CompleteSignUpProps {
|
||||
email: string;
|
||||
@@ -25,7 +26,7 @@ export const CompleteSignUp = ({
|
||||
setCountryCode,
|
||||
onCompleteSignUp,
|
||||
}: CompleteSignUpProps) => {
|
||||
const { t } = useTranslation('authentication');
|
||||
const { t, i18n } = useTranslation('authentication');
|
||||
const [error, setError] = useState<string>();
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -45,6 +46,12 @@ export const CompleteSignUp = ({
|
||||
handleValueError();
|
||||
};
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = replacePersianWithRealNumbers(event.target.value);
|
||||
|
||||
setValue(value);
|
||||
};
|
||||
|
||||
const handleValueError = () => {
|
||||
if (!value) {
|
||||
setError(t('loginForm.thisFieldIsRequired'));
|
||||
@@ -62,8 +69,13 @@ export const CompleteSignUp = ({
|
||||
if (!value || !isPhoneValid(countryCode, value)) {
|
||||
inputRef.current?.focus();
|
||||
} else {
|
||||
await sendSmsCall({ phoneNumber: countryCode + value });
|
||||
onCompleteSignUp(countryCode, value);
|
||||
let newValue = value;
|
||||
if (countryCode === '+98' && newValue.startsWith('09')) {
|
||||
newValue = newValue.substring(1);
|
||||
setValue(newValue);
|
||||
}
|
||||
await sendSmsCall({ phoneNumber: countryCode + newValue });
|
||||
onCompleteSignUp(countryCode, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,7 +105,7 @@ export const CompleteSignUp = ({
|
||||
inputRef={inputRef}
|
||||
label={t('completeSignUp.phoneNumber')}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
@@ -101,7 +113,16 @@ export const CompleteSignUp = ({
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
endAdornment: i18n.dir() === 'rtl' && (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={true}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
startAdornment: i18n.dir() === 'ltr' && (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
|
||||
@@ -67,9 +67,6 @@ 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);
|
||||
}
|
||||
|
||||
setLoginRegisterValue(newValue);
|
||||
|
||||
@@ -112,10 +109,20 @@ export function LoginRegisterForm({
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (validateInput(loginRegisterValue, authType, false)) {
|
||||
let newValue = loginRegisterValue;
|
||||
|
||||
if (
|
||||
authType === 'phone' &&
|
||||
countryCode === '+98' &&
|
||||
newValue.startsWith('09')
|
||||
) {
|
||||
newValue = newValue.substring(1);
|
||||
setLoginRegisterValue(newValue);
|
||||
}
|
||||
|
||||
const res = await execUserStatus({
|
||||
phoneNumber:
|
||||
authType === 'phone' ? countryCode + loginRegisterValue : undefined,
|
||||
email: authType === 'email' ? loginRegisterValue : undefined,
|
||||
phoneNumber: authType === 'phone' ? countryCode + newValue : undefined,
|
||||
email: authType === 'email' ? newValue : undefined,
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
@@ -123,7 +130,7 @@ export function LoginRegisterForm({
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
onLoginRegisterSubmit(loginRegisterValue, res.userStatus);
|
||||
onLoginRegisterSubmit(newValue, res.userStatus);
|
||||
} else {
|
||||
toast({ message: res.message, severity: 'error' });
|
||||
}
|
||||
|
||||
@@ -191,7 +191,6 @@ export function OtpVerifyForm({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useApi } from '@/hooks/useApi';
|
||||
export interface ChangePasswordProps {
|
||||
onEditInfo: () => void;
|
||||
onPasswordChanged: () => void;
|
||||
forgettedPasswordInfo: string;
|
||||
forgetPasswordInfo: string;
|
||||
infoType: AuthType;
|
||||
countryCode: CountryCode;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export interface ChangePasswordProps {
|
||||
export const ChangePassword = ({
|
||||
onEditInfo,
|
||||
onPasswordChanged,
|
||||
forgettedPasswordInfo,
|
||||
forgetPasswordInfo,
|
||||
infoType,
|
||||
countryCode,
|
||||
}: ChangePasswordProps) => {
|
||||
@@ -78,11 +78,9 @@ export const ChangePassword = ({
|
||||
confirmInputRef.current?.focus();
|
||||
} else {
|
||||
const apiRequest: ResetPasswordRequest = {
|
||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
||||
email: infoType === 'email' ? forgetPasswordInfo : undefined,
|
||||
phoneNumber:
|
||||
infoType === 'phone'
|
||||
? countryCode + forgettedPasswordInfo
|
||||
: undefined,
|
||||
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
|
||||
newPassword: passValue,
|
||||
confirmNewPassword: confirmPassValue,
|
||||
};
|
||||
@@ -138,7 +136,7 @@ export const ChangePassword = ({
|
||||
endIcon={<Icon Component={Edit2} />}
|
||||
onClick={onEditInfo}
|
||||
>
|
||||
{forgettedPasswordInfo}
|
||||
{forgetPasswordInfo}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import type { AuthType } from '../../types/authTypes';
|
||||
import { ForgettedPasswordInfo } from './ForgettedPasswordInfo';
|
||||
import { ForgetPasswordInfo } from './ForgetPasswordInfo';
|
||||
import { ForgetPasswordOtp } from './ForgetPasswordOtp';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
@@ -11,8 +11,7 @@ export const ForgetPasswordContainer = () => {
|
||||
const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState<
|
||||
'enterInfo' | 'verifyOtp' | 'setPassword'
|
||||
>('enterInfo');
|
||||
const [forgettedPasswordInfo, setForgettedPasswordInfo] =
|
||||
useState<string>('');
|
||||
const [forgetPasswordInfo, setForgetPasswordInfo] = useState<string>('');
|
||||
const [infoCountryCode, setInfoCountryCode] = useState<CountryCode>('+98');
|
||||
const [infoType, setInfoType] = useState<AuthType>('email');
|
||||
|
||||
@@ -35,11 +34,11 @@ export const ForgetPasswordContainer = () => {
|
||||
return (
|
||||
<>
|
||||
{forgetPassCurrentStep === 'enterInfo' && (
|
||||
<ForgettedPasswordInfo
|
||||
<ForgetPasswordInfo
|
||||
infoType={infoType}
|
||||
setInfoType={setInfoType}
|
||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||
setForgettedPasswordInfo={setForgettedPasswordInfo}
|
||||
forgetPasswordInfo={forgetPasswordInfo}
|
||||
setForgetPasswordInfo={setForgetPasswordInfo}
|
||||
onVerifyOtp={handleVerifyOtp}
|
||||
countryCode={infoCountryCode}
|
||||
setCountryCode={setInfoCountryCode}
|
||||
@@ -51,7 +50,7 @@ export const ForgetPasswordContainer = () => {
|
||||
countryCode={infoCountryCode}
|
||||
infoType={infoType}
|
||||
onEditInfo={handleEditInfo}
|
||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||
forgetPasswordInfo={forgetPasswordInfo}
|
||||
onOTPVerified={handleOtpVerified}
|
||||
/>
|
||||
)}
|
||||
@@ -59,7 +58,7 @@ export const ForgetPasswordContainer = () => {
|
||||
{forgetPassCurrentStep === 'setPassword' && (
|
||||
<ChangePassword
|
||||
onEditInfo={handleEditInfo}
|
||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||
forgetPasswordInfo={forgetPasswordInfo}
|
||||
onPasswordChanged={handlePasswordChanged}
|
||||
infoType={infoType}
|
||||
countryCode={infoCountryCode}
|
||||
|
||||
@@ -14,9 +14,9 @@ import { useToast } from '@rkheftan/harmony-ui';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||
|
||||
export interface ForgettedPasswordInfoProps {
|
||||
forgettedPasswordInfo: string;
|
||||
setForgettedPasswordInfo: Dispatch<string>;
|
||||
export interface ForgetPasswordInfoProps {
|
||||
forgetPasswordInfo: string;
|
||||
setForgetPasswordInfo: Dispatch<string>;
|
||||
infoType: AuthType;
|
||||
setInfoType: Dispatch<AuthType>;
|
||||
onVerifyOtp: (value: string) => void;
|
||||
@@ -24,15 +24,15 @@ export interface ForgettedPasswordInfoProps {
|
||||
setCountryCode: Dispatch<CountryCode>;
|
||||
}
|
||||
|
||||
export function ForgettedPasswordInfo({
|
||||
forgettedPasswordInfo,
|
||||
setForgettedPasswordInfo,
|
||||
export function ForgetPasswordInfo({
|
||||
forgetPasswordInfo,
|
||||
setForgetPasswordInfo,
|
||||
infoType,
|
||||
setInfoType,
|
||||
onVerifyOtp,
|
||||
countryCode,
|
||||
setCountryCode,
|
||||
}: ForgettedPasswordInfoProps) {
|
||||
}: ForgetPasswordInfoProps) {
|
||||
const { t } = useTranslation('authentication');
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -51,7 +51,7 @@ export function ForgettedPasswordInfo({
|
||||
if (newValue.startsWith('09')) {
|
||||
newValue = newValue.substring(1);
|
||||
}
|
||||
setForgettedPasswordInfo(newValue);
|
||||
setForgetPasswordInfo(newValue);
|
||||
|
||||
// If the new value contains only digits (or is empty), it's a phone number
|
||||
if (isNumeric(newValue)) {
|
||||
@@ -63,7 +63,7 @@ export function ForgettedPasswordInfo({
|
||||
|
||||
const handleBlur = () => {
|
||||
setTouched(true);
|
||||
validateInput(forgettedPasswordInfo, infoType);
|
||||
validateInput(forgetPasswordInfo, infoType);
|
||||
};
|
||||
|
||||
const validateInput = (
|
||||
@@ -87,13 +87,11 @@ export function ForgettedPasswordInfo({
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (validateInput(forgettedPasswordInfo, infoType, false)) {
|
||||
if (validateInput(forgetPasswordInfo, infoType, false)) {
|
||||
const sendCodeRequest: SendForgetPassCodeRequest = {
|
||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
||||
email: infoType === 'email' ? forgetPasswordInfo : undefined,
|
||||
phoneNumber:
|
||||
infoType === 'phone'
|
||||
? countryCode + forgettedPasswordInfo
|
||||
: undefined,
|
||||
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
|
||||
};
|
||||
const res = await sendForgetPassCodeCall(sendCodeRequest);
|
||||
|
||||
@@ -106,19 +104,18 @@ export function ForgettedPasswordInfo({
|
||||
});
|
||||
}
|
||||
|
||||
onVerifyOtp(forgettedPasswordInfo);
|
||||
onVerifyOtp(forgetPasswordInfo);
|
||||
} else {
|
||||
inputRef.current?.focus();
|
||||
validateInput(forgettedPasswordInfo, infoType);
|
||||
validateInput(forgetPasswordInfo, infoType);
|
||||
}
|
||||
};
|
||||
|
||||
const showAdornment =
|
||||
infoType === 'phone' && forgettedPasswordInfo.length > 0;
|
||||
const showAdornment = infoType === 'phone' && forgetPasswordInfo.length > 0;
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<Stack spacing={1}>
|
||||
<Stack component="form" onSubmit={handleSubmit} spacing={1}>
|
||||
<Typography variant="h5">
|
||||
{t('forgetPassword.forgetPassword')}
|
||||
</Typography>
|
||||
@@ -133,7 +130,7 @@ export function ForgettedPasswordInfo({
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
label={t('loginForm.emailOrPhoneLabel')}
|
||||
value={forgettedPasswordInfo}
|
||||
value={forgetPasswordInfo}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
@@ -157,7 +154,7 @@ export function ForgettedPasswordInfo({
|
||||
/>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Button loading={sendForgetPassCodeLoading} onClick={handleSubmit}>
|
||||
<Button loading={sendForgetPassCodeLoading} type="submit">
|
||||
{t('forgetPassword.confirm')}
|
||||
</Button>
|
||||
</Stack>
|
||||
@@ -18,7 +18,7 @@ import { Icon, useToast } from '@rkheftan/harmony-ui';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
|
||||
interface ForgetPasswordOtpProps {
|
||||
forgettedPasswordInfo: string;
|
||||
forgetPasswordInfo: string;
|
||||
infoType: AuthType;
|
||||
countryCode: CountryCode;
|
||||
onEditInfo: () => void;
|
||||
@@ -26,7 +26,7 @@ interface ForgetPasswordOtpProps {
|
||||
}
|
||||
|
||||
export function ForgetPasswordOtp({
|
||||
forgettedPasswordInfo,
|
||||
forgetPasswordInfo,
|
||||
infoType,
|
||||
countryCode,
|
||||
onEditInfo,
|
||||
@@ -69,9 +69,9 @@ export function ForgetPasswordOtp({
|
||||
|
||||
const handleResendOTPCode = async () => {
|
||||
const sendCodeRequest: SendForgetPassCodeRequest = {
|
||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
||||
email: infoType === 'email' ? forgetPasswordInfo : undefined,
|
||||
phoneNumber:
|
||||
infoType === 'phone' ? countryCode + forgettedPasswordInfo : undefined,
|
||||
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
|
||||
};
|
||||
await sendForgetPassCodeCall(sendCodeRequest);
|
||||
|
||||
@@ -93,11 +93,9 @@ export function ForgetPasswordOtp({
|
||||
|
||||
// Change setTimeout to api call
|
||||
const apiRequest: ConfirmForgetPassCodeRequest = {
|
||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
||||
email: infoType === 'email' ? forgetPasswordInfo : undefined,
|
||||
phoneNumber:
|
||||
infoType === 'phone'
|
||||
? countryCode + forgettedPasswordInfo
|
||||
: undefined,
|
||||
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
|
||||
code: otpCode,
|
||||
};
|
||||
|
||||
@@ -153,8 +151,8 @@ export function ForgetPasswordOtp({
|
||||
onClick={onEditInfo}
|
||||
>
|
||||
{infoType === 'phone'
|
||||
? countryCode + forgettedPasswordInfo
|
||||
: forgettedPasswordInfo}
|
||||
? countryCode + forgetPasswordInfo
|
||||
: forgetPasswordInfo}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ export function PhoneNumber() {
|
||||
setPhoneNumberTouched(false);
|
||||
setVerificationCodeError(undefined);
|
||||
setVerificationCodeTouched(false);
|
||||
setIsCodeSent(false);
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
|
||||
@@ -11,10 +11,11 @@ import { Edit2, TickCircle } from 'iconsax-react';
|
||||
import { CountDownTimer } from '@/components/CountDownTimer';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type PhoneEditFormProps } from '@/features/profile/types/settingsType';
|
||||
import { useRef } from 'react';
|
||||
import { useRef, type ChangeEvent } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { CountryCodeSelector } from '@/components/CountryCodeSelector';
|
||||
import { LTRTypography } from '@/components/common/LTRTypography';
|
||||
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||
|
||||
export default function PhoneEditForm({
|
||||
phoneNumber,
|
||||
@@ -42,6 +43,20 @@ export default function PhoneEditForm({
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = replacePersianWithRealNumbers(event.target.value);
|
||||
|
||||
setPhoneNumber(value);
|
||||
};
|
||||
|
||||
const onSendCode = () => {
|
||||
if (countryCode === '+98' && phoneNumber.startsWith('09')) {
|
||||
const newValue = phoneNumber.substring(1);
|
||||
setPhoneNumber(newValue);
|
||||
}
|
||||
handleSendCode();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ px: { sm: 4, xs: 2 }, my: 4 }}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
@@ -70,6 +85,7 @@ export default function PhoneEditForm({
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
autoFocus
|
||||
name="phoneNumber"
|
||||
label={t('settingForm.newPhoneNumber')}
|
||||
disabled={isCodeSent}
|
||||
@@ -80,30 +96,12 @@ export default function PhoneEditForm({
|
||||
onBlur={() => handleBlur('phoneNumber')}
|
||||
error={!!phoneNumberError}
|
||||
helperText={phoneNumberError}
|
||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||
onChange={handleChange}
|
||||
placeholder="09123456789"
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: isCodeSent ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setButtonState('default');
|
||||
setVerificationCode('');
|
||||
setIsCodeSent(false);
|
||||
}}
|
||||
edge="end"
|
||||
>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
variant="Bold"
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : (
|
||||
i18n.dir() === 'rtl' && (
|
||||
endAdornment:
|
||||
i18n.dir() === 'rtl' ? (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
@@ -112,18 +110,58 @@ export default function PhoneEditForm({
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
)
|
||||
),
|
||||
startAdornment: i18n.dir() === 'ltr' && (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
disabled={isCodeSent}
|
||||
show={true}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
) : (
|
||||
isCodeSent && (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setButtonState('default');
|
||||
setVerificationCode('');
|
||||
setIsCodeSent(false);
|
||||
}}
|
||||
edge="end"
|
||||
>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
variant="Bold"
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
startAdornment:
|
||||
i18n.dir() === 'ltr' ? (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
disabled={isCodeSent}
|
||||
show={true}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
) : (
|
||||
isCodeSent && (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setButtonState('default');
|
||||
setVerificationCode('');
|
||||
setIsCodeSent(false);
|
||||
}}
|
||||
edge="end"
|
||||
>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
variant="Bold"
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -144,7 +182,8 @@ export default function PhoneEditForm({
|
||||
<Button
|
||||
variant="text"
|
||||
loading={isSendingCode}
|
||||
onClick={handleSendCode}
|
||||
onClick={onSendCode}
|
||||
disabled={buttonState === 'counting'}
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
width: { xs: '100%', sm: 208 },
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
CircularProgress,
|
||||
Stack,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DeviceMessage, Logout } from 'iconsax-react';
|
||||
import { DeviceMessage, Logout, Mobile } from 'iconsax-react';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { PageWrapper } from '../components/PageWrapper';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
@@ -24,8 +23,6 @@ export function ActiveDevicesPage() {
|
||||
const { t, i18n } = useTranslation('setting');
|
||||
const [devices, setDevices] = useState<Device[]>([]);
|
||||
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
|
||||
const theme = useTheme();
|
||||
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
||||
const showToast = useToast();
|
||||
|
||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||
@@ -46,7 +43,8 @@ export function ActiveDevicesPage() {
|
||||
const formattedDevices = sessions.map((session: ApiSession) => ({
|
||||
id: session.key,
|
||||
timeAndDate: formatDate(session.created, i18n.language, t),
|
||||
deviceModel: `${session.deviceOs} ${session.deviceName}`,
|
||||
deviceOs: session.deviceOs,
|
||||
deviceName: session.deviceName,
|
||||
ip: session.ipAddress,
|
||||
current: session.key === currentKey,
|
||||
}));
|
||||
@@ -132,7 +130,9 @@ export function ActiveDevicesPage() {
|
||||
flexGrow: 0,
|
||||
width: 'auto',
|
||||
}}
|
||||
disabled={isLoadingProfile || isTerminating}
|
||||
disabled={
|
||||
isLoadingProfile || isTerminating || devices.length === 1
|
||||
}
|
||||
>
|
||||
{isTerminating
|
||||
? t('active.deleting')
|
||||
@@ -158,20 +158,23 @@ export function ActiveDevicesPage() {
|
||||
sx={{
|
||||
mx: { xs: 2, sm: 3, md: 4 },
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
{devices.map((device) => (
|
||||
<React.Fragment key={device.id}>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
flexDirection="row"
|
||||
key={device.id}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
||||
minHeight: 50,
|
||||
gap: 0.5,
|
||||
py: 1.5,
|
||||
}}
|
||||
>
|
||||
@@ -197,12 +200,16 @@ export function ActiveDevicesPage() {
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
Component={DeviceMessage}
|
||||
Component={
|
||||
device.deviceName === 'smartphone'
|
||||
? Mobile
|
||||
: DeviceMessage
|
||||
}
|
||||
size="medium"
|
||||
color="primary.main"
|
||||
/>
|
||||
<Typography variant="body2" noWrap>
|
||||
{device.deviceModel}
|
||||
{`${device.deviceOs} ${device.deviceName}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -217,85 +224,55 @@ export function ActiveDevicesPage() {
|
||||
>
|
||||
{device.ip}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexBasis: { xs: '100%', sm: 'auto' },
|
||||
mb: { xs: 1, sm: 0 },
|
||||
minWidth: { sm: '126px' },
|
||||
order: { xs: 4, sm: 4 },
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{device.current && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="medium"
|
||||
sx={{
|
||||
borderRadius: 12.5,
|
||||
border: '1px solid',
|
||||
borderColor: 'success.main',
|
||||
whiteSpace: 'nowrap',
|
||||
color: 'success.main',
|
||||
textTransform: 'none',
|
||||
maxWidth: '125px',
|
||||
'&.Mui-disabled': {
|
||||
color: 'success.main',
|
||||
borderColor: 'success.main',
|
||||
},
|
||||
}}
|
||||
disabled
|
||||
>
|
||||
{t('active.currentDevice')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexBasis: { xs: '100%', sm: 'auto' },
|
||||
mb: { xs: 1, sm: 0 },
|
||||
textAlign: { xs: 'left', sm: 'center' },
|
||||
minWidth: { sm: '138px' },
|
||||
order: { xs: 5, sm: 5 },
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<Icon Component={Logout} size="small" />}
|
||||
disabled={
|
||||
device.current || loadingDeleteIds.includes(device.id)
|
||||
}
|
||||
onClick={() => handleDeleteDevice(device.id)}
|
||||
sx={{
|
||||
color: 'error.main',
|
||||
borderRadius: 1,
|
||||
borderColor: 'error.main',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
textTransform: 'none',
|
||||
'& .MuiButton-startIcon': {
|
||||
marginRight: 0.5,
|
||||
marginLeft: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loadingDeleteIds.includes(device.id) ? (
|
||||
<CircularProgress size={20} color="error" />
|
||||
) : (
|
||||
t('active.deleteDevice')
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{isXsup && (
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }} />
|
||||
{device.current && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="medium"
|
||||
sx={{
|
||||
borderRadius: 12.5,
|
||||
border: '1px solid',
|
||||
borderColor: 'success.main',
|
||||
whiteSpace: 'nowrap',
|
||||
color: 'success.main',
|
||||
textTransform: 'none',
|
||||
maxWidth: '125px',
|
||||
'&.Mui-disabled': {
|
||||
color: 'success.main',
|
||||
borderColor: 'success.main',
|
||||
},
|
||||
}}
|
||||
disabled
|
||||
>
|
||||
{t('active.currentDevice')}
|
||||
</Button>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
||||
{!device.current && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<Icon Component={Logout} size="small" />}
|
||||
disabled={
|
||||
device.current || loadingDeleteIds.includes(device.id)
|
||||
}
|
||||
loading={loadingDeleteIds.includes(device.id)}
|
||||
onClick={() => handleDeleteDevice(device.id)}
|
||||
fullWidth={false}
|
||||
sx={{
|
||||
color: 'error.main',
|
||||
borderRadius: 1,
|
||||
borderColor: 'error.main',
|
||||
'& .MuiButton-startIcon': {
|
||||
marginRight: 0.5,
|
||||
marginLeft: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t('active.deleteDevice')}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { saveSettings } from '../api/settingsApi';
|
||||
import { useProfile } from '../hooks/useProfile';
|
||||
|
||||
type ThemeMode = 'light' | 'dark';
|
||||
type CalendarType = 'christian' | 'solar' | 'lunar';
|
||||
type CalendarType = 'christian' | 'jalali';
|
||||
|
||||
interface SettingsState {
|
||||
language: string;
|
||||
@@ -33,9 +33,8 @@ const languageOptions = [
|
||||
];
|
||||
|
||||
const calendarOptions: { key: CalendarType; apiValue: number }[] = [
|
||||
{ key: 'christian', apiValue: 1 },
|
||||
{ key: 'solar', apiValue: 2 },
|
||||
{ key: 'lunar', apiValue: 3 },
|
||||
{ key: 'jalali', apiValue: 1 },
|
||||
{ key: 'christian', apiValue: 2 },
|
||||
];
|
||||
|
||||
const themeApiMap: Record<ThemeMode, number> = { light: 1, dark: 2 };
|
||||
@@ -46,7 +45,7 @@ export function SettingPage() {
|
||||
|
||||
const [savedSettings, setSavedSettings] = useState<SettingsState>({
|
||||
language: i18n.language,
|
||||
calendar: 'solar',
|
||||
calendar: 'jalali',
|
||||
theme: mode === 'light' || mode === 'dark' ? mode : 'light',
|
||||
});
|
||||
const [draftSettings, setDraftSettings] =
|
||||
@@ -78,9 +77,9 @@ export function SettingPage() {
|
||||
theme: themeReverseMap[theme] || 'light',
|
||||
calendar:
|
||||
calendarOptions.find((c) => c.apiValue === calendarType)?.key ||
|
||||
'solar',
|
||||
'jalali',
|
||||
language:
|
||||
languageOptions.find((l) => l.apiValue === language)?.code || 'en',
|
||||
languageOptions.find((l) => l.apiValue === language)?.code || 'fa',
|
||||
};
|
||||
setSavedSettings(newSettings);
|
||||
setDraftSettings(newSettings);
|
||||
|
||||
@@ -19,7 +19,8 @@ export interface InfoRowData {
|
||||
export interface Device {
|
||||
id: string;
|
||||
timeAndDate: string;
|
||||
deviceModel: string;
|
||||
deviceName: 'smartphone' | 'desktop' | string;
|
||||
deviceOs: string;
|
||||
ip: string;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Suspense, type ReactNode } from 'react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { createBrowserRouter, type RouteObject } from 'react-router-dom';
|
||||
import { appRoutes, type RouteConfig } from './config';
|
||||
import { ProtectedRoute } from '@/components/routes/ProtectedRoute';
|
||||
import { Loading } from '@/components/routes/Loading';
|
||||
|
||||
/**
|
||||
* A recursive function to map our custom route config to the format
|
||||
@@ -10,15 +9,9 @@ import { Loading } from '@/components/routes/Loading';
|
||||
*/
|
||||
function mapRoutes(routes: RouteConfig[]): RouteObject[] {
|
||||
return routes.map((route) => {
|
||||
// Start with the base element, wrapped in Suspense for lazy loading
|
||||
let element: ReactNode = (
|
||||
<Suspense fallback={<Loading />}>{route.element}</Suspense>
|
||||
);
|
||||
|
||||
// Conditionally wrap the element in the specified layout
|
||||
// if (route.layout) {
|
||||
// element = <route.layout>{element}</route.layout>;
|
||||
// }
|
||||
// Remove the suspense from here and move to layout outlet
|
||||
// Avoid loading layout for rendering different children
|
||||
let element: ReactNode = route.element;
|
||||
|
||||
if (route.authorize) {
|
||||
element = <ProtectedRoute>{element}</ProtectedRoute>;
|
||||
|
||||
Reference in New Issue
Block a user