feat: add using enter instead pushing the button
This commit is contained in:
4
.env
4
.env
@@ -1,8 +1,8 @@
|
||||
VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com
|
||||
VITE_DEFUALT_AUTH_RETURN_URL=/setting/profile
|
||||
VITE_APP_URL=https://accounts.business-harmony.com
|
||||
VITE_API_URL=https://accounts.business-harmony.com/apiapp/api
|
||||
VITE_IDENTITY_URL=https://accounts.business-harmony.com/apiapp/connect/token
|
||||
VITE_API_URL=https://accounts.business-harmony.com/api
|
||||
VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token
|
||||
VITE_IDENTITY_CLIENT_ID=harmony_identity
|
||||
VITE_IDENTITY_SCOPE=openid profile offline_access
|
||||
IMAGE_BASE_URL=https://accounts.business-harmony.com/uploads/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, TextField, Typography } from '@mui/material';
|
||||
import { Box, Button, TextField, Typography } from '@mui/material';
|
||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||
import { useRef, useState, type Dispatch } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -69,47 +69,56 @@ export const CompleteSignUp = ({
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<Typography variant="h5" sx={{ mb: 0.5 }}>
|
||||
{t('completeSignUp.completeSignUp')}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}>
|
||||
{t(
|
||||
'completeSignUp.emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue',
|
||||
{ email },
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
label={t('completeSignUp.phoneNumber')}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={true}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!sendSmsLoading) void handleCompleteSignUp();
|
||||
}}
|
||||
sx={{ my: 4 }}
|
||||
/>
|
||||
>
|
||||
<Typography variant="h5" sx={{ mb: 0.5 }}>
|
||||
{t('completeSignUp.completeSignUp')}
|
||||
</Typography>
|
||||
|
||||
<Button loading={sendSmsLoading} onClick={handleCompleteSignUp}>
|
||||
{t('verify.confirmAndContinue')}
|
||||
</Button>
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}>
|
||||
{t(
|
||||
'completeSignUp.emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue',
|
||||
{ email },
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
label={t('completeSignUp.phoneNumber')}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={true}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ my: 4 }}
|
||||
/>
|
||||
|
||||
<Button loading={sendSmsLoading} type="submit">
|
||||
{t('verify.confirmAndContinue')}
|
||||
</Button>
|
||||
</Box>
|
||||
</AuthenticationCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
import type { LoginResult, PasswordLoginRequest } from '../../types/userTypes';
|
||||
import { Icon, useToast } from '@rkheftan/harmony-ui';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import {
|
||||
generateTokenWithPassword,
|
||||
type GenerateTokenResponse,
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
generateTokenWithGoogle,
|
||||
type GenerateTokenResponse,
|
||||
} from '../../api/identityAPI';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import type { AuthFactory } from '../../types/authTypes';
|
||||
|
||||
export interface GoogleAuthenticationProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Stack, TextField, Typography } from '@mui/material';
|
||||
import { Button, Stack, TextField, Typography, Box } from '@mui/material';
|
||||
import { useRef, useState, type Dispatch } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
@@ -120,51 +120,62 @@ export function LoginRegisterForm({
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<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, paddingX: 0 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={showAdornment}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
<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} onClick={handleSubmit}>
|
||||
{t('loginForm.submitButton')}
|
||||
</Button>
|
||||
|
||||
<GoogleAuthentication
|
||||
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, paddingX: 0 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={showAdornment}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ my: 4 }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Button loading={userStatusLoading} type="submit">
|
||||
{t('loginForm.submitButton')}
|
||||
</Button>
|
||||
|
||||
<GoogleAuthentication
|
||||
authFactory={authFactory}
|
||||
onGoogleAuthenticated={onGoogleAuthenticated}
|
||||
disabled={userStatusLoading}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</AuthenticationCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
generateTokenWithOtp,
|
||||
type GenerateTokenResponse,
|
||||
} from '../../api/identityAPI';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
||||
interface OtpVerifyFormProps {
|
||||
value: string;
|
||||
@@ -166,43 +165,54 @@ export function OtpVerifyForm({
|
||||
<Stack alignItems="center">
|
||||
<AuthenticationCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
component="form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!loginSignUpLoading) {
|
||||
void handleVerifyOTP();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">{t('verify.verify')}</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Icon Component={Edit2} />}
|
||||
onClick={onEditValue}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{authType === 'phone' ? countryCode + value : value}
|
||||
<Typography variant="h5">{t('verify.verify')}</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Icon Component={Edit2} />}
|
||||
onClick={onEditValue}
|
||||
>
|
||||
{authType === 'phone' ? countryCode + value : value}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{otpMessage}
|
||||
</Typography>
|
||||
|
||||
<DigitInput
|
||||
error={otpDigitInvalid || isStatusSuccess === false}
|
||||
success={isStatusSuccess === true}
|
||||
onChange={(value) => setOtpCode(value)}
|
||||
/>
|
||||
|
||||
<Button type="submit" loading={loginSignUpLoading}>
|
||||
{authMode === 'register'
|
||||
? t('verify.confirmAndContinue')
|
||||
: t('verify.confirmAndLogin')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{otpMessage}
|
||||
</Typography>
|
||||
|
||||
<DigitInput
|
||||
error={otpDigitInvalid || isStatusSuccess === false}
|
||||
success={isStatusSuccess === true}
|
||||
onChange={(value) => setOtpCode(value)}
|
||||
/>
|
||||
|
||||
<Button onClick={handleVerifyOTP} loading={loginSignUpLoading}>
|
||||
{authMode === 'register'
|
||||
? t('verify.confirmAndContinue')
|
||||
: t('verify.confirmAndLogin')}
|
||||
</Button>
|
||||
</AuthenticationCard>
|
||||
|
||||
<Stack
|
||||
|
||||
@@ -96,43 +96,54 @@ export function VerifyPhoneNumber({
|
||||
<Stack alignItems="center">
|
||||
<AuthenticationCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
component="form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!confirmSmsOtpLoading) {
|
||||
void handleVerifyOTP();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">{t('verify.verify')}</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Icon Component={Edit2} />}
|
||||
onClick={onEditValue}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{countryCode + value}
|
||||
<Typography variant="h5">{t('verify.verify')}</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Icon Component={Edit2} />}
|
||||
onClick={onEditValue}
|
||||
>
|
||||
{countryCode + value}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{t(
|
||||
'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<DigitInput
|
||||
error={otpDigitInvalid || isStatusSuccess === false}
|
||||
success={isStatusSuccess === true}
|
||||
onChange={(value) => setOtpCode(value)}
|
||||
/>
|
||||
|
||||
<Button type="submit" loading={confirmSmsOtpLoading}>
|
||||
{t('verify.confirmAndLogin')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{t(
|
||||
'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<DigitInput
|
||||
error={otpDigitInvalid || isStatusSuccess === false}
|
||||
success={isStatusSuccess === true}
|
||||
onChange={(value) => setOtpCode(value)}
|
||||
/>
|
||||
|
||||
<Button onClick={handleVerifyOTP} loading={confirmSmsOtpLoading}>
|
||||
{t('verify.confirmAndLogin')}
|
||||
</Button>
|
||||
</AuthenticationCard>
|
||||
|
||||
<Stack
|
||||
|
||||
@@ -116,54 +116,62 @@ export function ForgetPasswordOtp({
|
||||
<Stack alignItems="center">
|
||||
<AuthenticationCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
component="form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!confirmForgetPassCodeLoading) {
|
||||
void handleVerifyOTP();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{t('forgetPassword.forgetPassword')}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{t('forgetPassword.forgetPassword')}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Icon Component={Edit2} />}
|
||||
onClick={onEditInfo}
|
||||
>
|
||||
{infoType === 'phone'
|
||||
? countryCode + forgettedPasswordInfo
|
||||
: forgettedPasswordInfo}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{infoType === 'email'
|
||||
? t(
|
||||
'forgetPassword.anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress',
|
||||
)
|
||||
: t(
|
||||
'forgetPassword.anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber',
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Icon Component={Edit2} />}
|
||||
onClick={onEditInfo}
|
||||
>
|
||||
{infoType === 'phone'
|
||||
? countryCode + forgettedPasswordInfo
|
||||
: forgettedPasswordInfo}
|
||||
<DigitInput
|
||||
error={otpDigitInvalid || isStatusSuccess === false}
|
||||
success={isStatusSuccess === true}
|
||||
onChange={(value) => setOtpCode(value)}
|
||||
/>
|
||||
|
||||
<Button type="submit" loading={confirmForgetPassCodeLoading}>
|
||||
{t('forgetPassword.confirm')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{infoType === 'email'
|
||||
? t(
|
||||
'forgetPassword.anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress',
|
||||
)
|
||||
: t(
|
||||
'forgetPassword.anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber',
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<DigitInput
|
||||
error={otpDigitInvalid || isStatusSuccess === false}
|
||||
success={isStatusSuccess === true}
|
||||
onChange={(value) => setOtpCode(value)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleVerifyOTP}
|
||||
loading={confirmForgetPassCodeLoading}
|
||||
>
|
||||
{t('forgetPassword.confirm')}
|
||||
</Button>
|
||||
</AuthenticationCard>
|
||||
|
||||
<Stack
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { type SubmitProps } from '../../types/settingForm';
|
||||
|
||||
export function SubmitSection({ onSubmit, loading }: SubmitProps) {
|
||||
export function SubmitSection({ loading }: SubmitProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
|
||||
@@ -50,8 +50,9 @@ export function SubmitSection({ onSubmit, loading }: SubmitProps) {
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={onSubmit}
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
form="completionForm"
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '247px' },
|
||||
alignSelf: { xs: 'stretch', sm: 'center' },
|
||||
|
||||
@@ -390,80 +390,90 @@ export function UserCompletionPage() {
|
||||
maxWidth="730px"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
component="form"
|
||||
id="completionForm"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void handleSubmit();
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="h5" color="text.primary">
|
||||
{t('completion.title')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('completion.description')}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="h5" color="text.primary">
|
||||
{t('completion.title')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('completion.description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<PersonalInfoFields
|
||||
firstName={firstName}
|
||||
setFirstName={setFirstName}
|
||||
lastName={lastName}
|
||||
setLastName={setLastName}
|
||||
nationalId={nationalId}
|
||||
setNationalId={setNationalId}
|
||||
birthDate={birthDate}
|
||||
setBirthDate={setBirthDate}
|
||||
sex={sex}
|
||||
setSex={setSex}
|
||||
country={country}
|
||||
setCountry={setCountry}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
handleBlur={handleBlur}
|
||||
/>
|
||||
|
||||
<PasswordSection
|
||||
showPasswordSection={showPasswordSection}
|
||||
setShowPasswordSection={setShowPasswordSection}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
confirmPassword={confirmPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
matchPassword={matchPassword}
|
||||
hasNumber={hasNumber}
|
||||
hasMinLength={hasMinLength}
|
||||
hasUpperAndLower={hasUpperAndLower}
|
||||
hasSpecialChar={hasSpecialChar}
|
||||
validPassword={validPassword}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
handleBlur={handleBlur}
|
||||
/>
|
||||
|
||||
<EmailSection
|
||||
showEmail={showEmail}
|
||||
setShowEmail={setShowEmail}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
correctEmail={correctEmail}
|
||||
codeSent={codeSent}
|
||||
verificationCode={verificationCode}
|
||||
setVerificationCode={setVerificationCode}
|
||||
buttonState={buttonState}
|
||||
countdown={countdown}
|
||||
handleSendCode={handleSendCode}
|
||||
handleVerifyCode={handleVerifyCode}
|
||||
emailVerified={emailVerified}
|
||||
isVerifyingCode={isVerifyingCode}
|
||||
isSendingCode={isSendingCode}
|
||||
handleEditEmail={handleEditEmail}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
handleBlur={handleBlur}
|
||||
/>
|
||||
|
||||
<SubmitSection loading={isSubmitting} />
|
||||
</Box>
|
||||
|
||||
<PersonalInfoFields
|
||||
firstName={firstName}
|
||||
setFirstName={setFirstName}
|
||||
lastName={lastName}
|
||||
setLastName={setLastName}
|
||||
nationalId={nationalId}
|
||||
setNationalId={setNationalId}
|
||||
birthDate={birthDate}
|
||||
setBirthDate={setBirthDate}
|
||||
sex={sex}
|
||||
setSex={setSex}
|
||||
country={country}
|
||||
setCountry={setCountry}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
handleBlur={handleBlur}
|
||||
/>
|
||||
|
||||
<PasswordSection
|
||||
showPasswordSection={showPasswordSection}
|
||||
setShowPasswordSection={setShowPasswordSection}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
confirmPassword={confirmPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
matchPassword={matchPassword}
|
||||
hasNumber={hasNumber}
|
||||
hasMinLength={hasMinLength}
|
||||
hasUpperAndLower={hasUpperAndLower}
|
||||
hasSpecialChar={hasSpecialChar}
|
||||
validPassword={validPassword}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
handleBlur={handleBlur}
|
||||
/>
|
||||
|
||||
<EmailSection
|
||||
showEmail={showEmail}
|
||||
setShowEmail={setShowEmail}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
correctEmail={correctEmail}
|
||||
codeSent={codeSent}
|
||||
verificationCode={verificationCode}
|
||||
setVerificationCode={setVerificationCode}
|
||||
buttonState={buttonState}
|
||||
countdown={countdown}
|
||||
handleSendCode={handleSendCode}
|
||||
handleVerifyCode={handleVerifyCode}
|
||||
emailVerified={emailVerified}
|
||||
isVerifyingCode={isVerifyingCode}
|
||||
isSendingCode={isSendingCode}
|
||||
handleEditEmail={handleEditEmail}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
handleBlur={handleBlur}
|
||||
/>
|
||||
|
||||
<SubmitSection onSubmit={handleSubmit} loading={isSubmitting} />
|
||||
</Box>
|
||||
</AuthenticationCard>
|
||||
</FlexBox>
|
||||
|
||||
@@ -72,6 +72,5 @@ export interface PersonalInfoFieldsProps extends ValidationProps {
|
||||
}
|
||||
|
||||
export interface SubmitProps {
|
||||
onSubmit: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,12 @@ export function PasswordDialog({
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFormSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!loading) handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -69,120 +75,132 @@ export function PasswordDialog({
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
{changePassword && (
|
||||
<>
|
||||
<TextField
|
||||
label={t('securityForm.currentPassword')}
|
||||
type={showCurrent ? 'text' : 'password'}
|
||||
fullWidth
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowCurrent(!showCurrent)}>
|
||||
<Icon Component={showCurrent ? Eye : EyeSlash} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Link
|
||||
sx={{ fontSize: '15px', cursor: 'pointer' }}
|
||||
onClick={() => navigate('/forget-password')}
|
||||
>
|
||||
{t('securityForm.forgetPassword')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={t('securityForm.newPassword')}
|
||||
type={showNew ? 'text' : 'password'}
|
||||
fullWidth
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowNew(!showNew)}>
|
||||
<Icon Component={showNew ? Eye : EyeSlash} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
<Box component="form" onSubmit={onFormSubmit}>
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
/>
|
||||
|
||||
{showValidation && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<PasswordValidationItem
|
||||
isValid={hasNumber}
|
||||
label={t('securityForm.hasNumber')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasMinLength}
|
||||
label={t('securityForm.hasMinLength')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasUpperAndLower}
|
||||
label={t('securityForm.hasUpperAndLower')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasSpecialChar}
|
||||
label={t('securityForm.hasSpecialChar')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={t('securityForm.confirmPassword')}
|
||||
type={showConfirm ? 'text' : 'password'}
|
||||
fullWidth
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
error={confirmPassword.length > 0 && !matchPassword}
|
||||
helperText={
|
||||
confirmPassword.length > 0 && !matchPassword
|
||||
? t('securityForm.notCompatibility')
|
||||
: ' '
|
||||
}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowConfirm(!showConfirm)}>
|
||||
<Icon Component={showConfirm ? Eye : EyeSlash} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'center' }}>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{ height: 48, textTransform: 'none' }}
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
) : (
|
||||
t('securityForm.confirm')
|
||||
{changePassword && (
|
||||
<>
|
||||
<TextField
|
||||
label={t('securityForm.currentPassword')}
|
||||
type={showCurrent ? 'text' : 'password'}
|
||||
fullWidth
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => setShowCurrent(!showCurrent)}
|
||||
type="button"
|
||||
>
|
||||
<Icon Component={showCurrent ? Eye : EyeSlash} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Link
|
||||
sx={{ fontSize: '15px', cursor: 'pointer' }}
|
||||
onClick={() => navigate('/forget-password')}
|
||||
>
|
||||
{t('securityForm.forgetPassword')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
<TextField
|
||||
label={t('securityForm.newPassword')}
|
||||
type={showNew ? 'text' : 'password'}
|
||||
fullWidth
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => setShowNew(!showNew)}
|
||||
type="button"
|
||||
>
|
||||
<Icon Component={showNew ? Eye : EyeSlash} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{showValidation && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<PasswordValidationItem
|
||||
isValid={hasNumber}
|
||||
label={t('securityForm.hasNumber')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasMinLength}
|
||||
label={t('securityForm.hasMinLength')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasUpperAndLower}
|
||||
label={t('securityForm.hasUpperAndLower')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasSpecialChar}
|
||||
label={t('securityForm.hasSpecialChar')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={t('securityForm.confirmPassword')}
|
||||
type={showConfirm ? 'text' : 'password'}
|
||||
fullWidth
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
error={confirmPassword.length > 0 && !matchPassword}
|
||||
helperText={
|
||||
confirmPassword.length > 0 && !matchPassword
|
||||
? t('securityForm.notCompatibility')
|
||||
: ' '
|
||||
}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => setShowConfirm(!showConfirm)}
|
||||
type="button"
|
||||
>
|
||||
<Icon Component={showConfirm ? Eye : EyeSlash} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'center' }}>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{ height: 48, textTransform: 'none' }}
|
||||
variant="contained"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
) : (
|
||||
t('securityForm.confirm')
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useProfile } from '../../hooks/useProfile';
|
||||
|
||||
export function PersonalInformation() {
|
||||
const imageBaseUrl = import.meta.env.IMAGE_BASE_URL;
|
||||
|
||||
const { t } = useTranslation('setting');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
|
||||
@@ -29,18 +28,14 @@ export function PersonalInformation() {
|
||||
const [originalData, setOriginalData] = useState<InfoRowData | null>(null);
|
||||
const showToast = useToast();
|
||||
const infoRowEditRef = useRef<{ validateFields: () => boolean }>(null);
|
||||
|
||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||
|
||||
const { loading: isSavingProfile, execute: executeSaveProfile } =
|
||||
useApi(saveProfile);
|
||||
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
const profileData = await refetchProfile();
|
||||
|
||||
if (!profileData) return;
|
||||
|
||||
if (profileData.success) {
|
||||
const fetchedData = {
|
||||
firstName: profileData.firstName ?? '',
|
||||
@@ -66,7 +61,6 @@ export function PersonalInformation() {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -85,14 +79,11 @@ export function PersonalInformation() {
|
||||
if (!data) return;
|
||||
const isValid = infoRowEditRef.current?.validateFields?.();
|
||||
if (!isValid) return;
|
||||
|
||||
const saveData = await executeSaveProfile({
|
||||
data,
|
||||
imageUrl: uploadedImageFile,
|
||||
});
|
||||
|
||||
if (!saveData) return;
|
||||
|
||||
if (saveData?.success) {
|
||||
showToast({
|
||||
message: t('settingForm.successSaveProfile'),
|
||||
@@ -101,7 +92,6 @@ export function PersonalInformation() {
|
||||
setIsEditing(false);
|
||||
setOriginalData(data);
|
||||
setUploadedImageFile(null);
|
||||
// Force a refetch to update the cache with the new data
|
||||
refetchProfile({ force: true });
|
||||
} else {
|
||||
showToast({
|
||||
@@ -122,6 +112,7 @@ export function PersonalInformation() {
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
onClick={handleCancelClick}
|
||||
size="large"
|
||||
@@ -134,9 +125,11 @@ export function PersonalInformation() {
|
||||
{t('settingForm.rejectButton')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveClick}
|
||||
type="submit"
|
||||
form="profileForm"
|
||||
size="large"
|
||||
variant="contained"
|
||||
disabled={isSavingProfile}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
@@ -174,6 +167,42 @@ export function PersonalInformation() {
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : isEditing ? (
|
||||
<Box
|
||||
component="form"
|
||||
id="profileForm"
|
||||
noValidate
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isSavingProfile) void handleSaveClick();
|
||||
}}
|
||||
sx={{
|
||||
mx: { xs: 2, sm: 3, md: 4 },
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<ProfileImage
|
||||
initials={initials}
|
||||
uploadedImageUrl={uploadedImageUrl}
|
||||
onImageChange={(file) => {
|
||||
setUploadedImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () =>
|
||||
setUploadedImageUrl(reader.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
}}
|
||||
onRemoveImage={() => {
|
||||
setUploadedImageFile(null);
|
||||
setUploadedImageUrl(null);
|
||||
}}
|
||||
/>
|
||||
<InfoRowEdit ref={infoRowEditRef} data={data} setData={setData} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -185,37 +214,11 @@ export function PersonalInformation() {
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
{isEditing && (
|
||||
<ProfileImage
|
||||
initials={initials}
|
||||
uploadedImageUrl={uploadedImageUrl}
|
||||
onImageChange={(file) => {
|
||||
setUploadedImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () =>
|
||||
setUploadedImageUrl(reader.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
}}
|
||||
onRemoveImage={() => {
|
||||
setUploadedImageFile(null);
|
||||
setUploadedImageUrl(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{data &&
|
||||
(isEditing ? (
|
||||
<InfoRowEdit
|
||||
ref={infoRowEditRef}
|
||||
data={data}
|
||||
setData={setData}
|
||||
/>
|
||||
) : (
|
||||
<InfoRowDisplay
|
||||
data={data}
|
||||
uploadedImageUrl={uploadedImageUrl}
|
||||
initials={initials}
|
||||
/>
|
||||
))}
|
||||
<InfoRowDisplay
|
||||
data={data}
|
||||
uploadedImageUrl={uploadedImageUrl}
|
||||
initials={initials}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</CardContainer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
@@ -40,20 +40,21 @@ export function PhoneNumber() {
|
||||
|
||||
const { loading: isSendingCode, execute: executeSendCode } =
|
||||
useApi(sendVerificationCode);
|
||||
|
||||
const { loading: isVerifying, execute: executeConfirmCode } = useApi(
|
||||
confirmPhoneNumberCode,
|
||||
);
|
||||
|
||||
const { loading: isChangingPhone, execute: executeChangePhone } =
|
||||
useApi(changePhoneNumber);
|
||||
|
||||
const isBusy = useMemo(
|
||||
() => isVerifying || isSendingCode || isChangingPhone,
|
||||
[isVerifying, isSendingCode, isChangingPhone],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
const profileData = await refetchProfile();
|
||||
|
||||
if (!profileData) return;
|
||||
|
||||
if (profileData?.success) {
|
||||
setPhones([
|
||||
{
|
||||
@@ -69,36 +70,32 @@ export function PhoneNumber() {
|
||||
});
|
||||
}
|
||||
};
|
||||
loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
void loadProfile();
|
||||
}, [refetchProfile, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!phoneNumberTouched) return;
|
||||
if (!phoneNumber) {
|
||||
setPhoneNumberError(t('settingForm.thisFieldIsRequired'));
|
||||
return;
|
||||
}
|
||||
if (!isPhoneValid(countryCode, phoneNumber)) {
|
||||
const parsed = parsePhoneNumberFromString(countryCode + phoneNumber);
|
||||
const valid = Boolean(parsed?.isValid());
|
||||
if (!valid) {
|
||||
setPhoneNumberError(t('settingForm.phoneNumberIsInvalid'));
|
||||
} else {
|
||||
setPhoneNumberError(undefined);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [countryCode, phoneNumber, phoneNumberTouched]);
|
||||
}, [countryCode, phoneNumber, phoneNumberTouched, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!verificationCodeTouched) return;
|
||||
if (!verificationCode) {
|
||||
setVerificationCodeError(t('settingForm.verificationCodeRequired'));
|
||||
return;
|
||||
}
|
||||
setVerificationCodeError(undefined);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [verificationCode, verificationCodeTouched]);
|
||||
|
||||
const isPhoneValid = (code: string, phone: string) => {
|
||||
const phoneNum = parsePhoneNumberFromString(code + phone);
|
||||
return phoneNum?.isValid();
|
||||
};
|
||||
}, [verificationCode, verificationCodeTouched, t]);
|
||||
|
||||
const handleBlur = (key: string) => {
|
||||
if (key === 'phoneNumber') {
|
||||
@@ -126,14 +123,11 @@ export function PhoneNumber() {
|
||||
|
||||
const handleSendCode = async () => {
|
||||
setPhoneNumberTouched(true);
|
||||
if (phoneNumberError) return;
|
||||
|
||||
if (!phoneNumber || phoneNumberError) return;
|
||||
const result = await executeSendCode({
|
||||
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
|
||||
});
|
||||
|
||||
if (!result) return;
|
||||
|
||||
if (result?.success) {
|
||||
setButtonState('counting');
|
||||
setIsVerified(false);
|
||||
@@ -151,18 +145,13 @@ export function PhoneNumber() {
|
||||
|
||||
const handleVerifyCode = async () => {
|
||||
setVerificationCodeTouched(true);
|
||||
|
||||
if (verificationCodeError) return;
|
||||
|
||||
if (!verificationCode || verificationCodeError) return;
|
||||
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
|
||||
|
||||
const confirmResult = await executeConfirmCode({
|
||||
phoneNumber: fullPhoneNumber,
|
||||
verifyCode: verificationCode,
|
||||
});
|
||||
|
||||
if (!confirmResult) return;
|
||||
|
||||
if (!confirmResult.success || !confirmResult.confirm) {
|
||||
toast({
|
||||
message: confirmResult?.message || t('message.serverError'),
|
||||
@@ -170,19 +159,15 @@ export function PhoneNumber() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsVerified(true);
|
||||
toast({
|
||||
message: t('settingForm.phoneVerified'),
|
||||
severity: 'success',
|
||||
});
|
||||
|
||||
const changeResult = await executeChangePhone({
|
||||
phoneNumber: fullPhoneNumber,
|
||||
});
|
||||
|
||||
if (!changeResult) return;
|
||||
|
||||
if (changeResult?.success) {
|
||||
setPhones([
|
||||
{
|
||||
@@ -212,6 +197,8 @@ export function PhoneNumber() {
|
||||
isEditing={isEditing}
|
||||
toggleEdit={toggleEdit}
|
||||
t={t}
|
||||
formId="phoneForm"
|
||||
isSubmitting={isBusy}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -228,27 +215,40 @@ export function PhoneNumber() {
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : isEditing ? (
|
||||
<PhoneEditForm
|
||||
phoneNumber={phoneNumber}
|
||||
setPhoneNumber={setPhoneNumber}
|
||||
countryCode={countryCode}
|
||||
setCountryCode={setCountryCode}
|
||||
verificationCode={verificationCode}
|
||||
setVerificationCode={setVerificationCode}
|
||||
isVerified={isVerified}
|
||||
isVerifying={isVerifying || isSendingCode || isChangingPhone}
|
||||
isSendingCode={isSendingCode}
|
||||
buttonState={buttonState}
|
||||
setButtonState={setButtonState}
|
||||
handleSendCode={handleSendCode}
|
||||
handleVerifyClick={handleVerifyCode}
|
||||
phoneNumberError={phoneNumberTouched ? phoneNumberError : undefined}
|
||||
verificationCodeError={
|
||||
verificationCodeTouched ? verificationCodeError : undefined
|
||||
}
|
||||
handleBlur={handleBlur}
|
||||
phones={phones}
|
||||
/>
|
||||
<Box
|
||||
component="form"
|
||||
id="phoneForm"
|
||||
noValidate
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isBusy) void handleVerifyCode();
|
||||
}}
|
||||
>
|
||||
<PhoneEditForm
|
||||
phoneNumber={phoneNumber}
|
||||
setPhoneNumber={setPhoneNumber}
|
||||
countryCode={countryCode}
|
||||
setCountryCode={setCountryCode}
|
||||
verificationCode={verificationCode}
|
||||
setVerificationCode={setVerificationCode}
|
||||
isVerified={isVerified}
|
||||
isVerifying={isBusy}
|
||||
isSendingCode={isSendingCode}
|
||||
buttonState={buttonState}
|
||||
setButtonState={setButtonState}
|
||||
handleSendCode={handleSendCode}
|
||||
handleVerifyClick={handleVerifyCode}
|
||||
phoneNumberError={
|
||||
phoneNumberTouched ? phoneNumberError : undefined
|
||||
}
|
||||
verificationCodeError={
|
||||
verificationCodeTouched ? verificationCodeError : undefined
|
||||
}
|
||||
handleBlur={handleBlur}
|
||||
phones={phones}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<PhoneDisplay
|
||||
phones={phones.map((p) => {
|
||||
|
||||
@@ -8,36 +8,51 @@ export default function PhoneActionButtons({
|
||||
}: PhoneActionButtonsProps) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{isEditing && (
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
onClick={toggleEdit}
|
||||
size="large"
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
textTransform: 'none',
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
}}
|
||||
>
|
||||
{t('settingForm.rejectButton')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form="phoneForm"
|
||||
size="large"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
bgcolor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
whiteSpace: 'nowrap',
|
||||
textTransform: 'none',
|
||||
}}
|
||||
>
|
||||
{t('settingForm.saveButton')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={toggleEdit}
|
||||
size="large"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
borderRadius: 1,
|
||||
whiteSpace: 'nowrap',
|
||||
textTransform: 'none',
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
}}
|
||||
>
|
||||
{t('settingForm.rejectButton')}
|
||||
{t('settingForm.editPhoneNumber')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={toggleEdit}
|
||||
size="large"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
bgcolor: isEditing ? 'primary.main' : 'background.default',
|
||||
color: isEditing ? 'primary.contrastText' : 'primary.main',
|
||||
whiteSpace: 'nowrap',
|
||||
textTransform: 'none',
|
||||
}}
|
||||
>
|
||||
{isEditing
|
||||
? t('settingForm.saveButton')
|
||||
: t('settingForm.editPhoneNumber')}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,21 @@ export default function SocialMediaDialog({
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (isLoading) return;
|
||||
|
||||
if (dialogStep === 'enterEmail') {
|
||||
setTouched(true);
|
||||
if (!validateEmail(emailInput)) return;
|
||||
onSendCode();
|
||||
} else {
|
||||
onConfirmEmail();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -77,76 +92,80 @@ export default function SocialMediaDialog({
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography fontWeight="bold">{t('settingForm.newEmail')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.dialogHeader')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="email"
|
||||
value={emailInput}
|
||||
onChange={(e) => {
|
||||
setEmailInput(e.target.value);
|
||||
if (touched) validateEmail(e.target.value);
|
||||
<Box component="form" onSubmit={handleSubmit}>
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
onBlur={() => {
|
||||
setTouched(true);
|
||||
validateEmail(emailInput);
|
||||
}}
|
||||
label={t('settingForm.email')}
|
||||
placeholder="abc@email.com"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
autoFocus
|
||||
disabled={isLoading || dialogStep === 'enterCode'}
|
||||
error={touched && !!emailError}
|
||||
helperText={touched && emailError ? emailError : ' '}
|
||||
/>
|
||||
>
|
||||
<Box>
|
||||
<Typography fontWeight="bold">
|
||||
{t('settingForm.newEmail')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.dialogHeader')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{dialogStep === 'enterCode' && (
|
||||
<TextField
|
||||
fullWidth
|
||||
type="text"
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
label={t('settingForm.verificationCode')}
|
||||
autoComplete="one-time-code"
|
||||
inputMode="numeric"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
autoFocus
|
||||
disabled={isLoading}
|
||||
type="email"
|
||||
value={emailInput}
|
||||
onChange={(e) => {
|
||||
setEmailInput(e.target.value);
|
||||
if (touched) validateEmail(e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setTouched(true);
|
||||
validateEmail(emailInput);
|
||||
}}
|
||||
label={t('settingForm.email')}
|
||||
placeholder="abc@email.com"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
autoFocus={dialogStep === 'enterEmail'}
|
||||
disabled={isLoading || dialogStep === 'enterCode'}
|
||||
error={touched && !!emailError}
|
||||
helperText={touched && emailError ? emailError : ' '}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<Box sx={{ px: 3, pb: 2 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{ height: 48, textTransform: 'none', borderRadius: 2 }}
|
||||
variant="contained"
|
||||
disabled={isLoading || (dialogStep === 'enterEmail' && !emailInput)}
|
||||
onClick={dialogStep === 'enterEmail' ? onSendCode : onConfirmEmail}
|
||||
>
|
||||
{isLoading ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
) : dialogStep === 'enterEmail' ? (
|
||||
t('settingForm.verificationCodeButton')
|
||||
) : (
|
||||
t('settingForm.confirmAndSave')
|
||||
{dialogStep === 'enterCode' && (
|
||||
<TextField
|
||||
fullWidth
|
||||
type="text"
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
label={t('settingForm.verificationCode')}
|
||||
autoComplete="one-time-code"
|
||||
inputMode="numeric"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
autoFocus
|
||||
disabled={isLoading}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</DialogContent>
|
||||
|
||||
<Box sx={{ px: 3, pb: 2 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{ height: 48, textTransform: 'none', borderRadius: 2 }}
|
||||
variant="contained"
|
||||
type="submit"
|
||||
disabled={isLoading || (dialogStep === 'enterEmail' && !emailInput)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
) : dialogStep === 'enterEmail' ? (
|
||||
t('settingForm.verificationCodeButton')
|
||||
) : (
|
||||
t('settingForm.confirmAndSave')
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
export enum Gender {
|
||||
Male = 2,
|
||||
Female = 1,
|
||||
@@ -93,7 +95,9 @@ export interface EmailAccount {
|
||||
export interface PhoneActionButtonsProps {
|
||||
isEditing: boolean;
|
||||
toggleEdit: () => void;
|
||||
t: (key: string) => string;
|
||||
t: TFunction<'setting'>;
|
||||
formId?: string;
|
||||
isSubmitting?: boolean;
|
||||
}
|
||||
|
||||
export interface PhoneDisplayProps {
|
||||
|
||||
Reference in New Issue
Block a user