Merge pull request #32 from rkheftan/fix/enter

feat: add using enter instead pushing the button
This commit is contained in:
SajadMRjl
2025-09-14 11:59:13 +03:30
committed by GitHub
17 changed files with 674 additions and 558 deletions

2
.env
View File

@@ -1,6 +1,6 @@
VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com
VITE_DEFUALT_AUTH_RETURN_URL=/setting/profile VITE_DEFUALT_AUTH_RETURN_URL=/setting/profile
VITE_APP_URL=https://account.business-harmony.com VITE_APP_URL=https://accounts.business-harmony.com
VITE_API_URL=https://accounts.business-harmony.com/api VITE_API_URL=https://accounts.business-harmony.com/api
VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token
VITE_IDENTITY_CLIENT_ID=harmony_identity VITE_IDENTITY_CLIENT_ID=harmony_identity

View File

@@ -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 parsePhoneNumberFromString from 'libphonenumber-js';
import { useRef, useState, type Dispatch } from 'react'; import { useRef, useState, type Dispatch } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -78,47 +78,56 @@ export const CompleteSignUp = ({
return ( return (
<AuthenticationCard> <AuthenticationCard>
<Typography variant="h5" sx={{ mb: 0.5 }}> <Box
{t('completeSignUp.completeSignUp')} component="form"
</Typography> onSubmit={(e) => {
e.preventDefault();
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}> e.stopPropagation();
{t( if (!sendSmsLoading) void handleCompleteSignUp();
'completeSignUp.emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue',
{ email },
)}
</Typography>
<TextField
ref={textFieldRef}
inputRef={inputRef}
label={t('completeSignUp.phoneNumber')}
value={value}
onChange={handleChange}
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 }} >
/> <Typography variant="h5" sx={{ mb: 0.5 }}>
{t('completeSignUp.completeSignUp')}
</Typography>
<Button loading={sendSmsLoading} onClick={handleCompleteSignUp}> <Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}>
{t('verify.confirmAndContinue')} {t(
</Button> '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> </AuthenticationCard>
); );
}; };

View File

@@ -21,7 +21,6 @@ import {
import type { LoginResult, PasswordLoginRequest } from '../../types/userTypes'; import type { LoginResult, PasswordLoginRequest } from '../../types/userTypes';
import { Icon, useToast } from '@rkheftan/harmony-ui'; import { Icon, useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi'; import { useApi } from '@/hooks/useApi';
import { useAuth } from '@/hooks/useAuth';
import { import {
generateTokenWithPassword, generateTokenWithPassword,
type GenerateTokenResponse, type GenerateTokenResponse,

View File

@@ -14,7 +14,6 @@ import {
generateTokenWithGoogle, generateTokenWithGoogle,
type GenerateTokenResponse, type GenerateTokenResponse,
} from '../../api/identityAPI'; } from '../../api/identityAPI';
import { useAuth } from '@/hooks/useAuth';
import type { AuthFactory } from '../../types/authTypes'; import type { AuthFactory } from '../../types/authTypes';
export interface GoogleAuthenticationProps { export interface GoogleAuthenticationProps {

View File

@@ -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 { useRef, useState, type Dispatch } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { isNumeric } from '@/utils/regexes/isNumeric'; import { isNumeric } from '@/utils/regexes/isNumeric';
@@ -124,51 +124,62 @@ export function LoginRegisterForm({
return ( return (
<AuthenticationCard> <AuthenticationCard>
<Stack spacing={1}> <Box
<Typography variant="h5">{t('loginForm.title')}</Typography> component="form"
<Typography variant="body2" color="text.secondary"> onSubmit={(e) => {
{t('loginForm.description')} e.preventDefault();
</Typography> e.stopPropagation();
</Stack> if (!userStatusLoading) {
void handleSubmit();
<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 spacing={1}>
<Typography variant="h5">{t('loginForm.title')}</Typography>
<Typography variant="body2" color="text.secondary">
{t('loginForm.description')}
</Typography>
</Stack>
<Stack spacing={2}> <TextField
<Button loading={userStatusLoading} onClick={handleSubmit}> ref={textFieldRef}
{t('loginForm.submitButton')} inputRef={inputRef}
</Button> label={t('loginForm.emailOrPhoneLabel')}
value={loginRegisterValue}
<GoogleAuthentication onChange={handleInputChange}
authFactory={authFactory} onBlur={handleBlur}
onGoogleAuthenticated={onGoogleAuthenticated} error={inputError}
disabled={userStatusLoading} 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> </AuthenticationCard>
); );
} }

View File

@@ -18,7 +18,6 @@ import {
generateTokenWithOtp, generateTokenWithOtp,
type GenerateTokenResponse, type GenerateTokenResponse,
} from '../../api/identityAPI'; } from '../../api/identityAPI';
import { useAuth } from '@/hooks/useAuth';
interface OtpVerifyFormProps { interface OtpVerifyFormProps {
value: string; value: string;
@@ -166,43 +165,54 @@ export function OtpVerifyForm({
<Stack alignItems="center"> <Stack alignItems="center">
<AuthenticationCard> <AuthenticationCard>
<Box <Box
sx={{ component="form"
display: 'flex', onSubmit={(e) => {
flexDirection: 'row', e.preventDefault();
alignItems: 'center', e.stopPropagation();
justifyContent: 'space-between', if (!loginSignUpLoading) {
gap: 4, void handleVerifyOTP();
mb: 0.5, }
}} }}
> >
<Typography variant="h5">{t('verify.verify')}</Typography> <Box
sx={{
<Button display: 'flex',
variant="outlined" flexDirection: 'row',
size="large" alignItems: 'center',
sx={{ textTransform: 'lowercase', width: 'auto' }} justifyContent: 'space-between',
endIcon={<Icon Component={Edit2} />} gap: 4,
onClick={onEditValue} 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> </Button>
</Box> </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> </AuthenticationCard>
<Stack <Stack

View File

@@ -96,43 +96,54 @@ export function VerifyPhoneNumber({
<Stack alignItems="center"> <Stack alignItems="center">
<AuthenticationCard> <AuthenticationCard>
<Box <Box
sx={{ component="form"
display: 'flex', onSubmit={(e) => {
flexDirection: 'row', e.preventDefault();
alignItems: 'center', e.stopPropagation();
justifyContent: 'space-between', if (!confirmSmsOtpLoading) {
gap: 4, void handleVerifyOTP();
mb: 0.5, }
}} }}
> >
<Typography variant="h5">{t('verify.verify')}</Typography> <Box
sx={{
<Button display: 'flex',
variant="outlined" flexDirection: 'row',
size="large" alignItems: 'center',
sx={{ textTransform: 'lowercase', width: 'auto' }} justifyContent: 'space-between',
endIcon={<Icon Component={Edit2} />} gap: 4,
onClick={onEditValue} 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> </Button>
</Box> </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> </AuthenticationCard>
<Stack <Stack

View File

@@ -116,54 +116,62 @@ export function ForgetPasswordOtp({
<Stack alignItems="center"> <Stack alignItems="center">
<AuthenticationCard> <AuthenticationCard>
<Box <Box
sx={{ component="form"
display: 'flex', onSubmit={(e) => {
flexDirection: 'row', e.preventDefault();
alignItems: 'center', e.stopPropagation();
justifyContent: 'space-between', if (!confirmForgetPassCodeLoading) {
gap: 4, void handleVerifyOTP();
mb: 0.5, }
}} }}
> >
<Typography variant="h5"> <Box
{t('forgetPassword.forgetPassword')} 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> </Typography>
<Button <DigitInput
variant="outlined" error={otpDigitInvalid || isStatusSuccess === false}
size="large" success={isStatusSuccess === true}
sx={{ textTransform: 'lowercase', width: 'auto' }} onChange={(value) => setOtpCode(value)}
endIcon={<Icon Component={Edit2} />} />
onClick={onEditInfo}
> <Button type="submit" loading={confirmForgetPassCodeLoading}>
{infoType === 'phone' {t('forgetPassword.confirm')}
? countryCode + forgettedPasswordInfo
: forgettedPasswordInfo}
</Button> </Button>
</Box> </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> </AuthenticationCard>
<Stack <Stack

View File

@@ -11,7 +11,7 @@ import {
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { type SubmitProps } from '../../types/settingForm'; import { type SubmitProps } from '../../types/settingForm';
export function SubmitSection({ onSubmit, loading }: SubmitProps) { export function SubmitSection({ loading }: SubmitProps) {
const { t, i18n } = useTranslation('completionForm'); const { t, i18n } = useTranslation('completionForm');
const [openDialog, setOpenDialog] = useState(false); const [openDialog, setOpenDialog] = useState(false);
@@ -50,8 +50,9 @@ export function SubmitSection({ onSubmit, loading }: SubmitProps) {
<Button <Button
variant="contained" variant="contained"
onClick={onSubmit} type="submit"
disabled={loading} disabled={loading}
form="completionForm"
sx={{ sx={{
width: { xs: '100%', sm: '247px' }, width: { xs: '100%', sm: '247px' },
alignSelf: { xs: 'stretch', sm: 'center' }, alignSelf: { xs: 'stretch', sm: 'center' },

View File

@@ -390,80 +390,90 @@ export function UserCompletionPage() {
maxWidth="730px" maxWidth="730px"
> >
<Box <Box
sx={{ component="form"
display: 'flex', id="completionForm"
flexDirection: 'column', onSubmit={(e) => {
gap: 2, e.preventDefault();
e.stopPropagation();
void handleSubmit();
}} }}
> >
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}> <Box
<Typography variant="h5" color="text.primary"> sx={{
{t('completion.title')} display: 'flex',
</Typography> flexDirection: 'column',
<Typography variant="body2" color="text.secondary"> gap: 2,
{t('completion.description')} }}
</Typography> >
<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> </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> </Box>
</AuthenticationCard> </AuthenticationCard>
</FlexBox> </FlexBox>

View File

@@ -72,6 +72,5 @@ export interface PersonalInfoFieldsProps extends ValidationProps {
} }
export interface SubmitProps { export interface SubmitProps {
onSubmit: () => void;
loading: boolean; loading: boolean;
} }

View File

@@ -46,6 +46,12 @@ export function PasswordDialog({
const [showConfirm, setShowConfirm] = useState(false); const [showConfirm, setShowConfirm] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const onFormSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
e.stopPropagation();
if (!loading) handleSubmit();
};
return ( return (
<Dialog <Dialog
open={open} open={open}
@@ -69,120 +75,132 @@ export function PasswordDialog({
</Box> </Box>
</DialogTitle> </DialogTitle>
<DialogContent <Box component="form" onSubmit={onFormSubmit}>
sx={{ <DialogContent
display: 'flex', sx={{
flexDirection: 'column', display: 'flex',
gap: 2, flexDirection: 'column',
px: { xs: 2, sm: 3 }, 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>
),
}} }}
/>
{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 ? ( {changePassword && (
<CircularProgress size={24} color="inherit" /> <>
) : ( <TextField
t('securityForm.confirm') 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> </Dialog>
); );
} }

View File

@@ -14,7 +14,6 @@ import { useProfile } from '../../hooks/useProfile';
export function PersonalInformation() { export function PersonalInformation() {
const imageBaseUrl = import.meta.env.IMAGE_BASE_URL; const imageBaseUrl = import.meta.env.IMAGE_BASE_URL;
const { t } = useTranslation('setting'); const { t } = useTranslation('setting');
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null); const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
@@ -29,18 +28,14 @@ export function PersonalInformation() {
const [originalData, setOriginalData] = useState<InfoRowData | null>(null); const [originalData, setOriginalData] = useState<InfoRowData | null>(null);
const showToast = useToast(); const showToast = useToast();
const infoRowEditRef = useRef<{ validateFields: () => boolean }>(null); const infoRowEditRef = useRef<{ validateFields: () => boolean }>(null);
const { isLoadingProfile, refetchProfile } = useProfile(); const { isLoadingProfile, refetchProfile } = useProfile();
const { loading: isSavingProfile, execute: executeSaveProfile } = const { loading: isSavingProfile, execute: executeSaveProfile } =
useApi(saveProfile); useApi(saveProfile);
useEffect(() => { useEffect(() => {
const loadProfile = async () => { const loadProfile = async () => {
const profileData = await refetchProfile(); const profileData = await refetchProfile();
if (!profileData) return; if (!profileData) return;
if (profileData.success) { if (profileData.success) {
const fetchedData = { const fetchedData = {
firstName: profileData.firstName ?? '', firstName: profileData.firstName ?? '',
@@ -66,7 +61,6 @@ export function PersonalInformation() {
}); });
} }
}; };
loadProfile(); loadProfile();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@@ -85,14 +79,11 @@ export function PersonalInformation() {
if (!data) return; if (!data) return;
const isValid = infoRowEditRef.current?.validateFields?.(); const isValid = infoRowEditRef.current?.validateFields?.();
if (!isValid) return; if (!isValid) return;
const saveData = await executeSaveProfile({ const saveData = await executeSaveProfile({
data, data,
imageUrl: uploadedImageFile, imageUrl: uploadedImageFile,
}); });
if (!saveData) return; if (!saveData) return;
if (saveData?.success) { if (saveData?.success) {
showToast({ showToast({
message: t('settingForm.successSaveProfile'), message: t('settingForm.successSaveProfile'),
@@ -101,7 +92,6 @@ export function PersonalInformation() {
setIsEditing(false); setIsEditing(false);
setOriginalData(data); setOriginalData(data);
setUploadedImageFile(null); setUploadedImageFile(null);
// Force a refetch to update the cache with the new data
refetchProfile({ force: true }); refetchProfile({ force: true });
} else { } else {
showToast({ showToast({
@@ -122,6 +112,7 @@ export function PersonalInformation() {
{isEditing ? ( {isEditing ? (
<> <>
<Button <Button
type="button"
variant="text" variant="text"
onClick={handleCancelClick} onClick={handleCancelClick}
size="large" size="large"
@@ -134,9 +125,11 @@ export function PersonalInformation() {
{t('settingForm.rejectButton')} {t('settingForm.rejectButton')}
</Button> </Button>
<Button <Button
onClick={handleSaveClick} type="submit"
form="profileForm"
size="large" size="large"
variant="contained" variant="contained"
disabled={isSavingProfile}
sx={{ sx={{
textTransform: 'none', textTransform: 'none',
width: { xs: '100%', sm: 'auto' }, width: { xs: '100%', sm: 'auto' },
@@ -174,6 +167,42 @@ export function PersonalInformation() {
> >
<CircularProgress /> <CircularProgress />
</Box> </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 <Box
sx={{ sx={{
@@ -185,37 +214,11 @@ export function PersonalInformation() {
bgcolor: 'background.paper', bgcolor: 'background.paper',
}} }}
> >
{isEditing && ( <InfoRowDisplay
<ProfileImage data={data}
initials={initials} uploadedImageUrl={uploadedImageUrl}
uploadedImageUrl={uploadedImageUrl} initials={initials}
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}
/>
))}
</Box> </Box>
)} )}
</CardContainer> </CardContainer>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import parsePhoneNumberFromString from 'libphonenumber-js'; import parsePhoneNumberFromString from 'libphonenumber-js';
import { PageWrapper } from '../PageWrapper'; import { PageWrapper } from '../PageWrapper';
@@ -40,20 +40,21 @@ export function PhoneNumber() {
const { loading: isSendingCode, execute: executeSendCode } = const { loading: isSendingCode, execute: executeSendCode } =
useApi(sendVerificationCode); useApi(sendVerificationCode);
const { loading: isVerifying, execute: executeConfirmCode } = useApi( const { loading: isVerifying, execute: executeConfirmCode } = useApi(
confirmPhoneNumberCode, confirmPhoneNumberCode,
); );
const { loading: isChangingPhone, execute: executeChangePhone } = const { loading: isChangingPhone, execute: executeChangePhone } =
useApi(changePhoneNumber); useApi(changePhoneNumber);
const isBusy = useMemo(
() => isVerifying || isSendingCode || isChangingPhone,
[isVerifying, isSendingCode, isChangingPhone],
);
useEffect(() => { useEffect(() => {
const loadProfile = async () => { const loadProfile = async () => {
const profileData = await refetchProfile(); const profileData = await refetchProfile();
if (!profileData) return; if (!profileData) return;
if (profileData?.success) { if (profileData?.success) {
setPhones([ setPhones([
{ {
@@ -69,36 +70,32 @@ export function PhoneNumber() {
}); });
} }
}; };
loadProfile(); void loadProfile();
// eslint-disable-next-line react-hooks/exhaustive-deps }, [refetchProfile, toast]);
}, []);
useEffect(() => { useEffect(() => {
if (!phoneNumberTouched) return;
if (!phoneNumber) { if (!phoneNumber) {
setPhoneNumberError(t('settingForm.thisFieldIsRequired')); setPhoneNumberError(t('settingForm.thisFieldIsRequired'));
return; return;
} }
if (!isPhoneValid(countryCode, phoneNumber)) { const parsed = parsePhoneNumberFromString(countryCode + phoneNumber);
const valid = Boolean(parsed?.isValid());
if (!valid) {
setPhoneNumberError(t('settingForm.phoneNumberIsInvalid')); setPhoneNumberError(t('settingForm.phoneNumberIsInvalid'));
} else { } else {
setPhoneNumberError(undefined); setPhoneNumberError(undefined);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [countryCode, phoneNumber, phoneNumberTouched, t]);
}, [countryCode, phoneNumber, phoneNumberTouched]);
useEffect(() => { useEffect(() => {
if (!verificationCodeTouched) return;
if (!verificationCode) { if (!verificationCode) {
setVerificationCodeError(t('settingForm.verificationCodeRequired')); setVerificationCodeError(t('settingForm.verificationCodeRequired'));
return; return;
} }
setVerificationCodeError(undefined); setVerificationCodeError(undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps }, [verificationCode, verificationCodeTouched, t]);
}, [verificationCode, verificationCodeTouched]);
const isPhoneValid = (code: string, phone: string) => {
const phoneNum = parsePhoneNumberFromString(code + phone);
return phoneNum?.isValid();
};
const handleBlur = (key: string) => { const handleBlur = (key: string) => {
if (key === 'phoneNumber') { if (key === 'phoneNumber') {
@@ -126,14 +123,11 @@ export function PhoneNumber() {
const handleSendCode = async () => { const handleSendCode = async () => {
setPhoneNumberTouched(true); setPhoneNumberTouched(true);
if (phoneNumberError) return; if (!phoneNumber || phoneNumberError) return;
const result = await executeSendCode({ const result = await executeSendCode({
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''), phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
}); });
if (!result) return; if (!result) return;
if (result?.success) { if (result?.success) {
setButtonState('counting'); setButtonState('counting');
setIsVerified(false); setIsVerified(false);
@@ -151,18 +145,13 @@ export function PhoneNumber() {
const handleVerifyCode = async () => { const handleVerifyCode = async () => {
setVerificationCodeTouched(true); setVerificationCodeTouched(true);
if (!verificationCode || verificationCodeError) return;
if (verificationCodeError) return;
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, ''); const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
const confirmResult = await executeConfirmCode({ const confirmResult = await executeConfirmCode({
phoneNumber: fullPhoneNumber, phoneNumber: fullPhoneNumber,
verifyCode: verificationCode, verifyCode: verificationCode,
}); });
if (!confirmResult) return; if (!confirmResult) return;
if (!confirmResult.success || !confirmResult.confirm) { if (!confirmResult.success || !confirmResult.confirm) {
toast({ toast({
message: confirmResult?.message || t('message.serverError'), message: confirmResult?.message || t('message.serverError'),
@@ -170,19 +159,15 @@ export function PhoneNumber() {
}); });
return; return;
} }
setIsVerified(true); setIsVerified(true);
toast({ toast({
message: t('settingForm.phoneVerified'), message: t('settingForm.phoneVerified'),
severity: 'success', severity: 'success',
}); });
const changeResult = await executeChangePhone({ const changeResult = await executeChangePhone({
phoneNumber: fullPhoneNumber, phoneNumber: fullPhoneNumber,
}); });
if (!changeResult) return; if (!changeResult) return;
if (changeResult?.success) { if (changeResult?.success) {
setPhones([ setPhones([
{ {
@@ -212,6 +197,8 @@ export function PhoneNumber() {
isEditing={isEditing} isEditing={isEditing}
toggleEdit={toggleEdit} toggleEdit={toggleEdit}
t={t} t={t}
formId="phoneForm"
isSubmitting={isBusy}
/> />
} }
> >
@@ -228,27 +215,40 @@ export function PhoneNumber() {
<CircularProgress /> <CircularProgress />
</Box> </Box>
) : isEditing ? ( ) : isEditing ? (
<PhoneEditForm <Box
phoneNumber={phoneNumber} component="form"
setPhoneNumber={setPhoneNumber} id="phoneForm"
countryCode={countryCode} noValidate
setCountryCode={setCountryCode} onSubmit={(e) => {
verificationCode={verificationCode} e.preventDefault();
setVerificationCode={setVerificationCode} e.stopPropagation();
isVerified={isVerified} if (!isBusy) void handleVerifyCode();
isVerifying={isVerifying || isSendingCode || isChangingPhone} }}
isSendingCode={isSendingCode} >
buttonState={buttonState} <PhoneEditForm
setButtonState={setButtonState} phoneNumber={phoneNumber}
handleSendCode={handleSendCode} setPhoneNumber={setPhoneNumber}
handleVerifyClick={handleVerifyCode} countryCode={countryCode}
phoneNumberError={phoneNumberTouched ? phoneNumberError : undefined} setCountryCode={setCountryCode}
verificationCodeError={ verificationCode={verificationCode}
verificationCodeTouched ? verificationCodeError : undefined setVerificationCode={setVerificationCode}
} isVerified={isVerified}
handleBlur={handleBlur} isVerifying={isBusy}
phones={phones} isSendingCode={isSendingCode}
/> buttonState={buttonState}
setButtonState={setButtonState}
handleSendCode={handleSendCode}
handleVerifyClick={handleVerifyCode}
phoneNumberError={
phoneNumberTouched ? phoneNumberError : undefined
}
verificationCodeError={
verificationCodeTouched ? verificationCodeError : undefined
}
handleBlur={handleBlur}
phones={phones}
/>
</Box>
) : ( ) : (
<PhoneDisplay <PhoneDisplay
phones={phones.map((p) => { phones={phones.map((p) => {

View File

@@ -8,36 +8,51 @@ export default function PhoneActionButtons({
}: PhoneActionButtonsProps) { }: PhoneActionButtonsProps) {
return ( return (
<Box sx={{ display: 'flex', gap: 1 }}> <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 <Button
variant="text"
onClick={toggleEdit} onClick={toggleEdit}
size="large" size="large"
variant="outlined"
sx={{ sx={{
color: 'primary.main', borderRadius: 1,
whiteSpace: 'nowrap',
textTransform: 'none', textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
}} }}
> >
{t('settingForm.rejectButton')} {t('settingForm.editPhoneNumber')}
</Button> </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> </Box>
); );
} }

View File

@@ -54,6 +54,21 @@ export default function SocialMediaDialog({
return true; 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 ( return (
<Dialog <Dialog
open={open} open={open}
@@ -77,76 +92,80 @@ export default function SocialMediaDialog({
</Box> </Box>
</DialogTitle> </DialogTitle>
<DialogContent <Box component="form" onSubmit={handleSubmit}>
sx={{ <DialogContent
display: 'flex', sx={{
flexDirection: 'column', display: 'flex',
gap: 2, flexDirection: 'column',
px: { xs: 2, sm: 3 }, 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);
}} }}
onBlur={() => { >
setTouched(true); <Box>
validateEmail(emailInput); <Typography fontWeight="bold">
}} {t('settingForm.newEmail')}
label={t('settingForm.email')} </Typography>
placeholder="abc@email.com" <Typography variant="body2" color="text.secondary">
autoComplete="email" {t('settingForm.dialogHeader')}
inputMode="email" </Typography>
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }} </Box>
autoFocus
disabled={isLoading || dialogStep === 'enterCode'}
error={touched && !!emailError}
helperText={touched && emailError ? emailError : ' '}
/>
{dialogStep === 'enterCode' && (
<TextField <TextField
fullWidth fullWidth
type="text" type="email"
value={verificationCode} value={emailInput}
onChange={(e) => setVerificationCode(e.target.value)} onChange={(e) => {
label={t('settingForm.verificationCode')} setEmailInput(e.target.value);
autoComplete="one-time-code" if (touched) validateEmail(e.target.value);
inputMode="numeric" }}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} onBlur={() => {
autoFocus setTouched(true);
disabled={isLoading} 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 }}> {dialogStep === 'enterCode' && (
<Button <TextField
fullWidth fullWidth
sx={{ height: 48, textTransform: 'none', borderRadius: 2 }} type="text"
variant="contained" value={verificationCode}
disabled={isLoading || (dialogStep === 'enterEmail' && !emailInput)} onChange={(e) => setVerificationCode(e.target.value)}
onClick={dialogStep === 'enterEmail' ? onSendCode : onConfirmEmail} label={t('settingForm.verificationCode')}
> autoComplete="one-time-code"
{isLoading ? ( inputMode="numeric"
<CircularProgress size={24} color="inherit" /> sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
) : dialogStep === 'enterEmail' ? ( autoFocus
t('settingForm.verificationCodeButton') disabled={isLoading}
) : ( />
t('settingForm.confirmAndSave')
)} )}
</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> </Box>
</Dialog> </Dialog>
); );

View File

@@ -1,3 +1,5 @@
import type { TFunction } from 'i18next';
export enum Gender { export enum Gender {
Male = 2, Male = 2,
Female = 1, Female = 1,
@@ -93,7 +95,9 @@ export interface EmailAccount {
export interface PhoneActionButtonsProps { export interface PhoneActionButtonsProps {
isEditing: boolean; isEditing: boolean;
toggleEdit: () => void; toggleEdit: () => void;
t: (key: string) => string; t: TFunction<'setting'>;
formId?: string;
isSubmitting?: boolean;
} }
export interface PhoneDisplayProps { export interface PhoneDisplayProps {