diff --git a/package-lock.json b/package-lock.json
index c13cc97..8be8bed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,7 @@
"@mui/x-data-grid-premium": "^8.10.0",
"@mui/x-date-pickers": "^8.10.0",
"@rkheftan/harmony-ui": "^0.2.89",
- "@rollup/rollup-win32-x64-msvc": "^4.46.3",
+ "@rollup/rollup-darwin-arm64": "^4.46.3",
"axios": "^1.11.0",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.0.0-0",
@@ -1962,9 +1962,7 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
- "optional": true,
"os": [
"darwin"
]
@@ -2228,7 +2226,9 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
+ "optional": true,
"os": [
"win32"
]
diff --git a/package.json b/package.json
index 237bc04..d03bf6c 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"@mui/x-data-grid-premium": "^8.10.0",
"@mui/x-date-pickers": "^8.10.0",
"@rkheftan/harmony-ui": "^0.2.89",
- "@rollup/rollup-win32-x64-msvc": "^4.46.3",
+ "@rollup/rollup-darwin-arm64": "^4.46.3",
"axios": "^1.11.0",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.0.0-0",
diff --git a/public/locales/en/setting.json b/public/locales/en/setting.json
index cf75b15..37059cc 100644
--- a/public/locales/en/setting.json
+++ b/public/locales/en/setting.json
@@ -72,7 +72,9 @@
"errorConfirmCode": "Failed to confirm code",
"errorChangePhone": "Failed to change phone number",
"verificationCodeSent": "Verification code sent",
- "onlyOneAccountAllowed": "You can only link one email account."
+ "onlyOneAccountAllowed": "You can only link one email account.",
+ "editEmailOrGoogle": "Change Email / Google",
+ "emailText": "Your new email will replace your previous email (<1>{{email}}1>)"
},
"active": {
"activeDevices": "Active devices",
@@ -147,4 +149,4 @@
"description": "Encourage repeat purchases by sending discount codes and points to your customers."
}
}
-}
\ No newline at end of file
+}
diff --git a/public/locales/fa/setting.json b/public/locales/fa/setting.json
index 20c8cdd..9051565 100644
--- a/public/locales/fa/setting.json
+++ b/public/locales/fa/setting.json
@@ -72,7 +72,9 @@
"errorConfirmCode": "تایید کد با خطا مواجه شد",
"errorChangePhone": "تغییر تلفن همراه با خطا مواجه شد",
"verificationCodeSent": "کد تایید ارسال شد",
- "onlyOneAccountAllowed": "شما فقط میتوانید یک حساب ایمیل را متصل کنید"
+ "onlyOneAccountAllowed": "شما فقط میتوانید یک حساب ایمیل را متصل کنید",
+ "editEmailOrGoogle": "تغییر ایمیل / گوگل",
+ "emailText": "ایمیل جدید شما جایگزین ایمل قبلی ({{email}}) خواهد شد"
},
"active": {
"activeDevices": "نشست های فعال",
@@ -147,4 +149,4 @@
"description": "با ارسال کد تخفیف و امتیاز به مشتریان کسب و کارتان آنها را به خرید مجدد ترغیب کنید"
}
}
-}
\ No newline at end of file
+}
diff --git a/src/components/CountDownTimer.tsx b/src/components/CountDownTimer.tsx
index 6cb280f..12f8885 100644
--- a/src/components/CountDownTimer.tsx
+++ b/src/components/CountDownTimer.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { toLocaleDigits } from '@/utils/persianDigit';
@@ -14,27 +14,34 @@ export function CountDownTimer({
const { i18n } = useTranslation();
const [secondsLeft, setSecondsLeft] = useState(initialSeconds);
+ const onCompleteRef = useRef(onComplete);
+ useEffect(() => {
+ onCompleteRef.current = onComplete;
+ }, [onComplete]);
+
useEffect(() => {
setSecondsLeft(initialSeconds);
+ }, [initialSeconds]);
- const timer = setInterval(() => {
+ useEffect(() => {
+ if (secondsLeft <= 0) return;
+ const id = setInterval(() => {
setSecondsLeft((prev) => {
if (prev <= 1) {
- clearInterval(timer);
- onComplete?.();
+ clearInterval(id);
+ onCompleteRef.current?.();
return 0;
}
return prev - 1;
});
}, 1000);
-
- return () => clearInterval(timer);
- }, [initialSeconds, onComplete]);
+ return () => clearInterval(id);
+ }, [secondsLeft]);
const formatTime = (totalSeconds: number) => {
- const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
- const seconds = String(totalSeconds % 60).padStart(2, '0');
- return toLocaleDigits(`${minutes}:${seconds}`, i18n.language);
+ const m = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
+ const s = String(totalSeconds % 60).padStart(2, '0');
+ return toLocaleDigits(`${m}:${s}`, i18n.language);
};
return {formatTime(secondsLeft)};
diff --git a/src/features/profile/components/security/PasswordDialog.tsx b/src/features/profile/components/security/PasswordDialog.tsx
index f83ca8c..cda6322 100644
--- a/src/features/profile/components/security/PasswordDialog.tsx
+++ b/src/features/profile/components/security/PasswordDialog.tsx
@@ -92,7 +92,7 @@ export function PasswordDialog({
fullWidth
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
- sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
+ sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1 }, mt: 2 }}
InputProps={{
endAdornment: (
@@ -121,7 +121,7 @@ export function PasswordDialog({
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
- sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
+ sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1 }, mt: 2 }}
InputProps={{
endAdornment: (
@@ -169,7 +169,7 @@ export function PasswordDialog({
? t('securityForm.notCompatibility')
: ' '
}
- sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
+ sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1 } }}
InputProps={{
endAdornment: (
diff --git a/src/features/profile/components/userInformation/SocialMedia.tsx b/src/features/profile/components/userInformation/SocialMedia.tsx
index 0e4eaac..56a2aa5 100644
--- a/src/features/profile/components/userInformation/SocialMedia.tsx
+++ b/src/features/profile/components/userInformation/SocialMedia.tsx
@@ -1,13 +1,13 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { CardContainer } from '@/components/CardContainer';
+import { Box, CircularProgress, Button } from '@mui/material';
import { PageWrapper } from '../PageWrapper';
+import { CardContainer } from '@/components/CardContainer';
import SocialMediaList from './socialMedia/SocialMediaList';
-import SocialMediaMenu from './socialMedia/SocialMediaMenu';
-import SocialMediaDialog from './socialMedia/SocialMediaDialog';
-import useMediaQuery from '@mui/material/useMediaQuery';
-import type { Theme } from '@mui/material/styles';
-import { Box, CircularProgress } from '@mui/material';
+import SocialEditForm from './socialMedia/SocialEditForm';
+import SocialMediaMenu, {
+ type SocialChoice,
+} from './socialMedia/SocialMediaMenu';
import { useApi } from '@/hooks/useApi';
import {
sendEmailCode,
@@ -20,51 +20,46 @@ import { useProfile } from '../../hooks/useProfile';
export function SocialMedia() {
const { t } = useTranslation('setting');
+ const toast = useToast();
- const [openDialog, setOpenDialog] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
const [emailInput, setEmailInput] = useState('');
const [verificationCode, setVerificationCode] = useState('');
- const [dialogStep, setDialogStep] = useState<'enterEmail' | 'enterCode'>(
- 'enterEmail',
+ const [email, setEmail] = useState([]);
+ const [emailError, setEmailError] = useState();
+ const [verificationCodeError, setVerificationCodeError] = useState<
+ string | undefined
+ >();
+ const [isCodeSent, setIsCodeSent] = useState(false);
+ const [buttonState, setButtonState] = useState<'default' | 'counting'>(
+ 'default',
);
- const [emailList, setEmailList] = useState([]);
- const [formError, setFormError] = useState(null);
- const toast = useToast();
+ const [isVerified, setIsVerified] = useState(false);
const { isLoadingProfile, refetchProfile } = useProfile();
const { loading: isSendingCode, execute: executeSendCode } =
useApi(sendEmailCode);
-
const { loading: isConfirming, execute: executeConfirmCode } =
useApi(confirmEmailCode);
-
const { loading: isChangingEmail, execute: executeChangeEmail } =
useApi(changeEmail);
- const fullScreen = useMediaQuery((theme: Theme) =>
- theme?.breakpoints.down('sm'),
+ const isBusy = useMemo(
+ () => isSendingCode || isConfirming || isChangingEmail,
+ [isSendingCode, isConfirming, isChangingEmail],
);
- const downMd = useMediaQuery((theme: Theme) => theme?.breakpoints.down('md'));
- const computedMaxWidth = (fullScreen ? 'xs' : downMd ? 'sm' : 'md') as
- | 'xs'
- | 'sm'
- | 'md'
- | 'lg'
- | 'xl';
+ const isSubmitting = isConfirming || isChangingEmail;
useEffect(() => {
const loadProfile = async () => {
const profileData = await refetchProfile();
-
if (!profileData) return;
if (profileData?.success) {
const { email } = profileData;
-
if (!email) return;
-
- setEmailList([
+ setEmail([
{
email,
provider: email.includes('@gmail.') ? 'google' : 'email',
@@ -72,33 +67,48 @@ export function SocialMedia() {
},
]);
} else {
- toast({
- message: profileData.message,
- severity: 'error',
- });
+ toast({ message: profileData.message, severity: 'error' });
}
};
+ void loadProfile();
+ }, [refetchProfile, toast]);
- loadProfile();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const resetDialog = () => {
- setOpenDialog(false);
+ const resetState = () => {
setEmailInput('');
setVerificationCode('');
- setFormError(null);
- setDialogStep('enterEmail');
+ setEmailError(undefined);
+ setVerificationCodeError(undefined);
+ setIsCodeSent(false);
+ setButtonState('default');
+ setIsVerified(false);
+ };
+
+ const startEmailEdit = () => {
+ resetState();
+ setIsEditing(true);
+ };
+
+ const buttonLabel = email.length
+ ? t('settingForm.editEmailOrGoogle')
+ : t('settingForm.addEmailOrSocialButton');
+
+ const validateEmail = (value: string) => {
+ if (!value) {
+ setEmailError(t('settingForm.thisFieldIsRequired'));
+ return false;
+ }
+ const ok = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
+ if (!ok) {
+ setEmailError(t('settingForm.emailIsInvalid'));
+ return false;
+ }
+ setEmailError(undefined);
+ return true;
};
const handleSendCode = async () => {
- if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(emailInput)) {
- setFormError(t('settingForm.emailIsInvalid'));
- return;
- }
- setFormError(null);
+ if (!validateEmail(emailInput)) return;
const sendCodeData = await executeSendCode({ email: emailInput });
-
if (!sendCodeData) return;
if (sendCodeData.success) {
@@ -106,28 +116,27 @@ export function SocialMedia() {
message: t('settingForm.verificationCodeSent'),
severity: 'success',
});
- setDialogStep('enterCode');
-
- setTimeout(
- () => {
- resetDialog();
- },
- 3 * 60 * 1000,
- );
+ setIsCodeSent(true);
+ setButtonState('counting');
+ } else {
+ toast({
+ message: sendCodeData.message || t('settingForm.error'),
+ severity: 'error',
+ });
}
};
- const handleConfirmAndChangeEmail = async () => {
- if (verificationCode.length < 4) {
- setFormError(t('settingForm.verificationCodeRequired'));
+ const handleVerifyClick = async () => {
+ if (!verificationCode || verificationCode.length < 4) {
+ setVerificationCodeError(t('settingForm.verificationCodeRequired'));
return;
}
- setFormError(null);
+ setVerificationCodeError(undefined);
+
const confirmData = await executeConfirmCode({
email: emailInput,
verifyCode: verificationCode,
});
-
if (!confirmData) return;
if (!confirmData.success || !confirmData.confirm) {
@@ -138,33 +147,53 @@ export function SocialMedia() {
return;
}
- if (emailList.length >= 1) {
- toast({
- message: t('settingForm.onlyOneAccountAllowed'),
- severity: 'error',
- });
- return;
- }
-
const changeEmailData = await executeChangeEmail({ email: emailInput });
if (!changeEmailData) return;
if (changeEmailData.success) {
- setEmailList([
+ setEmail([
{
email: emailInput,
provider: emailInput.includes('@gmail.') ? 'google' : 'email',
time: t('settingForm.justNow'),
},
]);
-
+ setIsVerified(true);
toast({
message: t('settingForm.emailChangedSuccessfully'),
severity: 'success',
});
-
- resetDialog();
+ setIsEditing(false);
refetchProfile({ force: true });
+ } else {
+ toast({
+ message: changeEmailData.message || t('settingForm.error'),
+ severity: 'error',
+ });
+ }
+ };
+
+ const handleBlur = (field: 'email' | 'verificationCode') => {
+ if (field === 'email') validateEmail(emailInput);
+ if (field === 'verificationCode') {
+ if (!verificationCode || verificationCode.length < 4) {
+ setVerificationCodeError(t('settingForm.verificationCodeRequired'));
+ } else {
+ setVerificationCodeError(undefined);
+ }
+ }
+ };
+
+ const onMenuSelect = (choice: SocialChoice) => {
+ if (choice === 'email') {
+ startEmailEdit();
+ return;
+ }
+ if (choice === 'google') {
+ toast({
+ message: t('settingForm.googleLinkComingSoon'),
+ severity: 'info',
+ });
}
};
@@ -173,8 +202,51 @@ export function SocialMedia() {
setOpenDialog(true)} />
+ isEditing ? (
+
+
+
+
+ ) : (
+
+ )
}
>
{isLoadingProfile ? (
@@ -184,30 +256,45 @@ export function SocialMedia() {
justifyContent: 'center',
alignItems: 'center',
p: 4,
- minHeight: '100px',
+ minHeight: '150px',
}}
>
+ ) : isEditing ? (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ if (!isBusy) void handleVerifyClick();
+ }}
+ >
+ ({ address: e.email }))}
+ verificationCode={verificationCode}
+ setVerificationCode={setVerificationCode}
+ isVerified={isVerified}
+ isVerifying={isBusy}
+ isSendingCode={isSendingCode}
+ isCodeSent={isCodeSent}
+ setIsCodeSent={setIsCodeSent}
+ buttonState={buttonState}
+ setButtonState={setButtonState}
+ handleSendCode={handleSendCode}
+ handleVerifyClick={handleVerifyClick}
+ handleBlur={handleBlur}
+ emailError={emailError}
+ verificationCodeError={verificationCodeError}
+ />
+
) : (
-
+
)}
-
);
diff --git a/src/features/profile/components/userInformation/socialMedia/SocialEditForm.tsx b/src/features/profile/components/userInformation/socialMedia/SocialEditForm.tsx
new file mode 100644
index 0000000..e09bd9e
--- /dev/null
+++ b/src/features/profile/components/userInformation/socialMedia/SocialEditForm.tsx
@@ -0,0 +1,199 @@
+import {
+ Box,
+ Typography,
+ TextField,
+ Button,
+ IconButton,
+ InputAdornment,
+ CircularProgress,
+} from '@mui/material';
+import { Edit2, TickCircle } from 'iconsax-react';
+import { CountDownTimer } from '@/components/CountDownTimer';
+import { Icon } from '@rkheftan/harmony-ui';
+import { useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+
+type SocialEditFormProps = {
+ email: string;
+ setEmail: (v: string) => void;
+ emails: { address: string }[];
+ verificationCode: string;
+ setVerificationCode: (v: string) => void;
+ isVerified: boolean;
+ isVerifying: boolean;
+ isSendingCode: boolean;
+ isCodeSent: boolean;
+ setIsCodeSent: (v: boolean) => void;
+ buttonState: 'default' | 'counting';
+ setButtonState: (v: 'default' | 'counting') => void;
+ handleSendCode: () => void;
+ handleVerifyClick: () => void;
+ handleBlur: (field: 'email' | 'verificationCode') => void;
+ emailError?: string;
+ verificationCodeError?: string;
+};
+
+export default function SocialEditForm({
+ email,
+ setEmail,
+ emails,
+ verificationCode,
+ setVerificationCode,
+ isVerified,
+ isVerifying,
+ isSendingCode,
+ isCodeSent,
+ setIsCodeSent,
+ buttonState,
+ setButtonState,
+ handleSendCode,
+ handleVerifyClick,
+ handleBlur,
+ emailError,
+ verificationCodeError,
+}: SocialEditFormProps) {
+ const { t } = useTranslation('setting');
+
+ const textFieldRef = useRef(null);
+ const inputRef = useRef(null);
+
+ return (
+
+
+
+ {t('settingForm.editEmailOrGoogle')}
+
+
+ {t('settingForm.emailText', {
+ email: emails.map((e) => e.address).join(', '),
+ })}
+
+
+
+
+ handleBlur('email')}
+ error={!!emailError}
+ helperText={emailError}
+ onChange={(e) => setEmail(e.target.value)}
+ placeholder="abc@email.com"
+ autoComplete="email"
+ inputMode="email"
+ disabled={isCodeSent}
+ slotProps={{
+ input: {
+ endAdornment: isCodeSent ? (
+
+ {
+ setButtonState('default');
+ setVerificationCode('');
+ setIsCodeSent(false);
+ }}
+ edge="end"
+ >
+
+
+
+ ) : undefined,
+ },
+ }}
+ />
+
+ {isVerified ? (
+
+
+ {t('settingForm.successButton')}
+
+ ) : (
+
+ )}
+
+
+ {isCodeSent && !isSendingCode && !isVerified && (
+
+ handleBlur('verificationCode')}
+ onChange={(e) =>
+ setVerificationCode(e.target.value.replace(/\D/g, ''))
+ }
+ placeholder={t('settingForm.verificationCode')}
+ autoComplete="one-time-code"
+ inputMode="numeric"
+ />
+
+
+
+ )}
+
+ );
+}
diff --git a/src/features/profile/components/userInformation/socialMedia/SocialMediaDialog.tsx b/src/features/profile/components/userInformation/socialMedia/SocialMediaDialog.tsx
deleted file mode 100644
index 7f130f3..0000000
--- a/src/features/profile/components/userInformation/socialMedia/SocialMediaDialog.tsx
+++ /dev/null
@@ -1,172 +0,0 @@
-import React, { type ReactElement, type ElementType, useState } from 'react';
-import {
- Box,
- Button,
- CircularProgress,
- Dialog,
- DialogContent,
- DialogTitle,
- IconButton,
- TextField,
- Typography,
-} from '@mui/material';
-import Slide from '@mui/material/Slide';
-import type { TransitionProps } from '@mui/material/transitions';
-import { CloseCircle } from 'iconsax-react';
-import { Icon } from '@rkheftan/harmony-ui';
-import { type SocialMediaDialogProps } from '@/features/profile/types/settingsType';
-import { isEmail } from '@/utils/regexes/isEmail';
-
-const MobileSlide = React.forwardRef(function MobileSlide(
- props: TransitionProps & { children: ReactElement },
- ref: React.Ref,
-) {
- return ;
-});
-
-export default function SocialMediaDialog({
- open,
- onClose,
- t,
- emailInput,
- setEmailInput,
- fullScreen,
- verificationCode,
- setVerificationCode,
- isLoading,
- dialogStep,
- onSendCode,
- onConfirmEmail,
-}: SocialMediaDialogProps) {
- const [emailError, setEmailError] = useState();
- const [touched, setTouched] = useState(false);
-
- const validateEmail = (value: string) => {
- if (!value) {
- setEmailError(t('settingForm.thisFieldIsRequired'));
- return false;
- }
- if (!isEmail(value)) {
- setEmailError(t('settingForm.emailIsInvalid'));
- return false;
- }
- setEmailError(undefined);
- return true;
- };
-
- const handleSubmit: React.FormEventHandler = (e) => {
- e.preventDefault();
- e.stopPropagation();
-
- if (isLoading) return;
-
- if (dialogStep === 'enterEmail') {
- setTouched(true);
- if (!validateEmail(emailInput)) return;
- onSendCode();
- } else {
- onConfirmEmail();
- }
- };
-
- return (
-
- );
-}
diff --git a/src/features/profile/components/userInformation/socialMedia/SocialMediaList.tsx b/src/features/profile/components/userInformation/socialMedia/SocialMediaList.tsx
index 2f0a0e0..bfbb3fa 100644
--- a/src/features/profile/components/userInformation/socialMedia/SocialMediaList.tsx
+++ b/src/features/profile/components/userInformation/socialMedia/SocialMediaList.tsx
@@ -19,12 +19,7 @@ export default function SocialMediaList({ emailList }: SocialMediaListProps) {
}}
>
- {item.provider === 'google' && (
+ {item.provider === 'google' ? (
- )}
- {item.provider === 'email' && (
+ ) : (
string;
+ onSelect: (choice: SocialChoice) => void;
+ buttonLabel: string;
+};
+
+export default function SocialMediaMenu({ t, onSelect, buttonLabel }: Props) {
const [anchor, setAnchor] = useState(null);
const openMenu = Boolean(anchor);
- const [open, setOpen] = useState(false);
-
- const handleClickMenu = (e: React.MouseEvent) => {
- setOpen(true);
- setAnchor(e.currentTarget);
- };
- const handleCloseMenu = () => {
- setOpen(false);
- setAnchor(null);
- };
return (
-
-
+
+ {buttonLabel}
+
+