From 16fa6615c9585f38d496320d00d312c9f563ea3d Mon Sep 17 00:00:00 2001 From: Sajad Mirjalili Date: Thu, 21 Aug 2025 14:51:56 +0330 Subject: [PATCH] fix: dashboard api calls, multi profile fetch --- public/locales/en/sideNav.json | 7 +- src/components/Loading.tsx | 1 - src/components/NavigateWithToast.tsx | 2 +- src/config/i18n.ts | 4 +- .../routes/UserCompletionPage.tsx | 5 +- .../types/completionFormApiTypes.ts | 4 +- .../components/security/PasswordDialog.tsx | 12 +- .../components/security/PasswordSecurity.tsx | 135 ++++---- .../components/security/RecentLogins.tsx | 67 ++-- .../userInformation/PersonalInformation.tsx | 145 ++++---- .../userInformation/PhoneNumber.tsx | 322 ++++++++---------- .../userInformation/SocialMedia.tsx | 208 +++++------ .../personalInformation/InfoRowEdit.tsx | 4 +- .../phoneNumber/PhoneDisplay.tsx | 4 +- .../phoneNumber/PhoneEditForm.tsx | 83 ++--- .../profile/contexts/ProfileContext.ts | 13 + src/features/profile/hooks/useProfile.ts | 10 + .../profile/providers/ProfileProvider.tsx | 62 ++++ .../profile/routes/ActiveDevicesPage.tsx | 111 +++--- src/features/profile/routes/SettingPage.tsx | 107 +++--- src/features/profile/types/settingsApiType.ts | 4 +- src/features/profile/types/settingsType.ts | 14 +- src/providers/RtlProvider.tsx | 2 +- src/routes/config.tsx | 9 +- 24 files changed, 656 insertions(+), 679 deletions(-) create mode 100644 src/features/profile/contexts/ProfileContext.ts create mode 100644 src/features/profile/hooks/useProfile.ts create mode 100644 src/features/profile/providers/ProfileProvider.tsx diff --git a/public/locales/en/sideNav.json b/public/locales/en/sideNav.json index bdb6e8a..4057675 100644 --- a/public/locales/en/sideNav.json +++ b/public/locales/en/sideNav.json @@ -2,13 +2,12 @@ "side": { "account": "Account", "personalInfo": "Personal information", - "phoneNumber": "Phone number", "contactInfo": "Contact info", "email": "Email", "security": "Security", "password": "Password", - "recentSessions": "Recent Sessions", - "activeDevices": "Active devices", - "settings": "Settings" + "recentSessions": "Recent sessions", + "activeSessions": "Active sessions", + "setting": "Settings" } } diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx index 6790bd5..d0e8b82 100644 --- a/src/components/Loading.tsx +++ b/src/components/Loading.tsx @@ -6,7 +6,6 @@ export const Loading = () => { sx={{ alignItems: 'center', justifyContent: 'center', - backgroundColor: 'background.default', position: 'fixed', inset: '0', zIndex: (t) => t.zIndex.tooltip + 1, diff --git a/src/components/NavigateWithToast.tsx b/src/components/NavigateWithToast.tsx index 51a133f..d62e277 100644 --- a/src/components/NavigateWithToast.tsx +++ b/src/components/NavigateWithToast.tsx @@ -1,5 +1,5 @@ import { useToast, type ToastOptions } from '@rkheftan/harmony-ui'; -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Navigate } from 'react-router-dom'; diff --git a/src/config/i18n.ts b/src/config/i18n.ts index 1723690..97fd431 100644 --- a/src/config/i18n.ts +++ b/src/config/i18n.ts @@ -9,9 +9,9 @@ i18n .use(initReactI18next) // Passes i18n down to react-i18next .init({ supportedLngs: ['en', 'fa'], // Supported languages - fallbackLng: 'en', + fallbackLng: 'fa', detection: { - order: ['localStorage', 'cookie', 'navigator'], + order: ['localStorage', 'cookie'], caches: ['localStorage', 'cookie'], lookupLocalStorage: 'language', // The key to use in localStorage }, diff --git a/src/features/authentication/routes/UserCompletionPage.tsx b/src/features/authentication/routes/UserCompletionPage.tsx index 57e43e5..66bfb79 100644 --- a/src/features/authentication/routes/UserCompletionPage.tsx +++ b/src/features/authentication/routes/UserCompletionPage.tsx @@ -188,13 +188,13 @@ export function UserCompletionPage() { firstName, lastName, gender: sex || 0, - nationalId, + nationalCode: nationalId, savePassword: showPasswordSection, password: showPasswordSection ? password : undefined, saveEmail: showEmail, email: showEmail ? email : undefined, birthDate, - country, + countryCode: country, }); if (res) { @@ -351,6 +351,7 @@ export function UserCompletionPage() { if (Object.keys(touched).length > 0) { validateForm(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ firstName, lastName, diff --git a/src/features/authentication/types/completionFormApiTypes.ts b/src/features/authentication/types/completionFormApiTypes.ts index 7dc8821..db01f60 100644 --- a/src/features/authentication/types/completionFormApiTypes.ts +++ b/src/features/authentication/types/completionFormApiTypes.ts @@ -18,9 +18,9 @@ export interface CompleteUserInfoPayload { lastName: string; // TODO: fix this gender: 0 | 1 | 2; - nationalId: string; + nationalCode: string; birthDate: Date | null; - country: string; + countryCode: string; savePassword?: boolean; password?: string; saveEmail?: boolean; diff --git a/src/features/profile/components/security/PasswordDialog.tsx b/src/features/profile/components/security/PasswordDialog.tsx index 2aefd9b..5920786 100644 --- a/src/features/profile/components/security/PasswordDialog.tsx +++ b/src/features/profile/components/security/PasswordDialog.tsx @@ -9,7 +9,6 @@ import { Button, Link, CircularProgress, - Typography, InputAdornment, } from '@mui/material'; import { CloseCircle, Eye, EyeSlash } from 'iconsax-react'; @@ -18,6 +17,7 @@ import { type PasswordDialogProps } from '../../types/settingsType'; import { PasswordValidationItem } from './PasswordValidation'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; export function PasswordDialog({ open, @@ -34,13 +34,13 @@ export function PasswordDialog({ loading, handleSubmit, changePassword, - apiError, - t, hasMinLength, hasNumber, hasUpperAndLower, hasSpecialChar, }: PasswordDialogProps) { + const { t } = useTranslation('setting'); + const [showCurrent, setShowCurrent] = useState(false); const [showNew, setShowNew] = useState(false); const [showConfirm, setShowConfirm] = useState(false); @@ -77,12 +77,6 @@ export function PasswordDialog({ px: { xs: 2, sm: 3 }, }} > - {apiError && ( - - {apiError} - - )} - {changePassword && ( <> { - if (profileData?.success) { - setChangePasswordUI(profileData.hasPassword || false); - } - }, [profileData]); + const loadProfile = async () => { + const profileData = await refetchProfile(); + + if (!profileData) return; + + if (profileData.success) { + setChangePasswordUI(profileData.hasPassword || false); + } else { + showToast({ + message: profileData.message || t('message.serverError'), + severity: 'error', + }); + } + }; + + loadProfile(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { if (password) { @@ -79,30 +78,6 @@ export function PasswordSecurity() { } }, [password, validPassword]); - useEffect(() => { - if (addData?.success || changeData?.success) { - showToast({ - message: changePasswordUI - ? t('securityForm.passwordChanged') - : t('securityForm.passwordAdded'), - severity: 'success', - }); - if (!changePasswordUI) { - setChangePasswordUI(true); - } - setOpen(false); - setPassword(''); - setConfirmPassword(''); - setCurrentPassword(''); - } else if (addData || changeData) { - showToast({ - message: - addData?.message || changeData?.message || t('securityForm.error'), - severity: 'error', - }); - } - }, [addData, changeData, changePasswordUI, showToast, t]); - const handleOpen = () => setOpen(true); const handleClose = () => setOpen(false); @@ -115,26 +90,48 @@ export function PasswordSecurity() { return; } if (changePasswordUI) { - await executeChangePassword({ + const changeData = await executeChangePassword({ oldPassword: currentPassword, newPassword: password, }); + + if (!changeData) return; + + if (!changeData.success) { + showToast({ + message: changeData?.message || t('securityForm.error'), + severity: 'error', + }); + return; + } } else { - await executeAddPassword({ newPassword: password }); + const addData = await executeAddPassword({ newPassword: password }); + + if (!addData) return; + + if (!addData.success) { + showToast({ + message: addData?.message || t('securityForm.error'), + severity: 'error', + }); + return; + } } + showToast({ + message: changePasswordUI + ? t('securityForm.passwordChanged') + : t('securityForm.passwordAdded'), + severity: 'success', + }); + if (!changePasswordUI) { + setChangePasswordUI(true); + } + setOpen(false); + setPassword(''); + setConfirmPassword(''); + setCurrentPassword(''); }; - const getErrorMessage = (error: unknown): string | null => { - if (!error) return null; - if (error instanceof Error) return error.message; - return String(error); - }; - - const apiError = useMemo( - () => addError || changeError, - [addError, changeError], - ); - return ( } > - {isFetching ? ( + {isLoadingProfile ? ( - ) : fetchError ? ( - - - {getErrorMessage(fetchError)} - - ) : ( {changePasswordUI ? ( @@ -208,8 +199,6 @@ export function PasswordSecurity() { loading={isAdding || isChanging} handleSubmit={handlePasswordSubmit} changePassword={changePasswordUI} - apiError={getErrorMessage(apiError)} - t={t} hasMinLength={hasMinLength} hasNumber={hasNumber} hasUpperAndLower={hasUpperAndLower} diff --git a/src/features/profile/components/security/RecentLogins.tsx b/src/features/profile/components/security/RecentLogins.tsx index 3feefca..7ce6ade 100644 --- a/src/features/profile/components/security/RecentLogins.tsx +++ b/src/features/profile/components/security/RecentLogins.tsx @@ -9,11 +9,10 @@ import { import { useTranslation } from 'react-i18next'; import { CardContainer } from '@/components/CardContainer'; import { PageWrapper } from '../PageWrapper'; -import { fetchProfile } from '../../api/settingsApi'; import type { LoginLog } from '../../types/settingsApiType'; -import { useApi } from '@/hooks/useApi'; import { formatDate } from '@/utils/formatSessionDate'; import { useToast } from '@rkheftan/harmony-ui'; +import { useProfile } from '../../hooks/useProfile'; export function RecentLogins() { const { t, i18n } = useTranslation('setting'); @@ -22,31 +21,43 @@ export function RecentLogins() { const isXsup = useMediaQuery(theme.breakpoints.up('xs')); const showToast = useToast(); - const { - data: profileData, - loading: isLoading, - error: fetchError, - } = useApi(fetchProfile, { immediate: true }); + const { isLoadingProfile, refetchProfile } = useProfile(); + + // useEffect(() => { + // if (profileData?.success && Array.isArray(profileData.loginLogs)) { + // setLogs(profileData.loginLogs); + // } + // }, [profileData]); + + // useEffect(() => { + // if (fetchError) { + // showToast({ + // message: getErrorMessage(fetchError) || t('active.errorFetch'), + // severity: 'error', + // }); + // } + // }, [fetchError, t, showToast]); useEffect(() => { - if (profileData?.success && Array.isArray(profileData.loginLogs)) { - setLogs(profileData.loginLogs); - } - }, [profileData]); + const loadProfile = async () => { + const profileData = await refetchProfile(); - useEffect(() => { - if (fetchError) { - showToast({ - message: getErrorMessage(fetchError) || t('active.errorFetch'), - severity: 'error', - }); - } - }, [fetchError, t, showToast]); - const getErrorMessage = (error: unknown): string | null => { - if (!error) return null; - if (error instanceof Error) return error.message; - return String(error); - }; + if (!profileData) return; + + if (profileData.success) { + if (!Array.isArray(profileData.loginLogs)) return; + setLogs(profileData.loginLogs); + } else { + showToast({ + message: profileData.message || t('active.errorFetch'), + severity: 'error', + }); + } + }; + + loadProfile(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( @@ -54,7 +65,7 @@ export function RecentLogins() { title={t('securityForm.recentLogins')} subtitle={t('securityForm.description')} > - {isLoading ? ( + {isLoadingProfile ? ( - ) : fetchError ? ( - - - {getErrorMessage(fetchError) || t('active.errorFetch')} - - ) : ( {logs.map((log, index) => ( diff --git a/src/features/profile/components/userInformation/PersonalInformation.tsx b/src/features/profile/components/userInformation/PersonalInformation.tsx index eaf6078..d5de493 100644 --- a/src/features/profile/components/userInformation/PersonalInformation.tsx +++ b/src/features/profile/components/userInformation/PersonalInformation.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from 'react'; -import { Box, Button, Typography, CircularProgress } from '@mui/material'; +import { Box, Button, CircularProgress } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { CardContainer } from '@/components/CardContainer'; import { ProfileImage } from './personalInformation/ProfileImage'; @@ -8,10 +8,13 @@ import { InfoRowEdit } from './personalInformation/InfoRowEdit'; import { PageWrapper } from '../PageWrapper'; import { useApi } from '@/hooks/useApi'; import { Gender, type InfoRowData } from '../../types/settingsType'; -import { fetchProfile, saveProfile } from '../../api/settingsApi'; +import { saveProfile } from '../../api/settingsApi'; import { useToast } from '@rkheftan/harmony-ui'; +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(null); @@ -27,52 +30,46 @@ export function PersonalInformation() { const showToast = useToast(); const infoRowEditRef = useRef<{ validateFields: () => boolean }>(null); - const { - data: profileData, - loading: isLoadingProfile, - error: fetchProfileError, - } = useApi(fetchProfile, { immediate: true }); - const { - data: saveData, - loading: isSavingProfile, - error: saveProfileError, - execute: executeSaveProfile, - } = useApi(saveProfile); + const { isLoadingProfile, refetchProfile } = useProfile(); + + const { loading: isSavingProfile, execute: executeSaveProfile } = + useApi(saveProfile); useEffect(() => { - if (profileData?.success) { - const fetchedData = { - firstName: profileData.firstName ?? '', - lastName: profileData.lastName ?? '', - nationalCode: profileData.nationalCode ?? '', - gender: Object.values(Gender).includes(profileData.gender as Gender) - ? (profileData.gender as Gender) - : Gender.None, - country: profileData.countryCode ?? '', - }; - setData(fetchedData); - setOriginalData(fetchedData); - const imageBaseUrl = import.meta.env.IMAGE_BASE_URL; - setUploadedImageUrl( - profileData.profileImageUrl - ? `${imageBaseUrl}${profileData.profileImageUrl}` - : null, - ); - setUploadedImageFile(null); - } - }, [profileData]); + const loadProfile = async () => { + const profileData = await refetchProfile(); - useEffect(() => { - if (saveData?.success) { - showToast({ - message: t('settingForm.successSaveProfile'), - severity: 'success', - }); - setIsEditing(false); - setOriginalData(data); - setUploadedImageFile(null); - } - }, [saveData, data, showToast, t]); + if (!profileData) return; + + if (profileData.success) { + const fetchedData = { + firstName: profileData.firstName ?? '', + lastName: profileData.lastName ?? '', + nationalCode: profileData.nationalCode ?? '', + gender: Object.values(Gender).includes(profileData.gender as Gender) + ? (profileData.gender as Gender) + : Gender.None, + country: profileData.countryCode ?? '', + }; + setData(fetchedData); + setOriginalData(fetchedData); + setUploadedImageUrl( + profileData.profileImageUrl + ? `${imageBaseUrl}${profileData.profileImageUrl}` + : null, + ); + setUploadedImageFile(null); + } else { + showToast({ + message: profileData.message || t('message.serverError'), + severity: 'error', + }); + } + }; + + loadProfile(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const initials = `${data?.firstName?.trim()[0] || ''}‌${data?.lastName?.trim()[0] || ''}`; @@ -84,36 +81,35 @@ export function PersonalInformation() { setUploadedImageFile(null); }; - const handleSaveClick = () => { + const handleSaveClick = async () => { if (!data) return; const isValid = infoRowEditRef.current?.validateFields?.(); if (!isValid) return; - executeSaveProfile({ data, imageUrl: uploadedImageFile }); - }; - const getErrorMessage = (error: unknown): string | null => { - if (!error) return null; - if (error instanceof Error) return error.message; - return String(error); - }; + const saveData = await executeSaveProfile({ + data, + imageUrl: uploadedImageFile, + }); - useEffect(() => { - if (saveProfileError) + if (!saveData) return; + + if (saveData?.success) { showToast({ - message: - getErrorMessage(saveProfileError) || t('settingForm.errorSave'), + message: t('settingForm.successSaveProfile'), + severity: 'success', + }); + setIsEditing(false); + setOriginalData(data); + setUploadedImageFile(null); + // Force a refetch to update the cache with the new data + refetchProfile({ force: true }); + } else { + showToast({ + message: saveData.message || t('message.serverError'), severity: 'error', }); - }, [saveProfileError, showToast, t]); - - useEffect(() => { - if (fetchProfileError) - showToast({ - message: - getErrorMessage(fetchProfileError) || t('settingForm.errorFetch'), - severity: 'error', - }); - }, [fetchProfileError, showToast, t]); + } + }; return ( @@ -163,14 +159,6 @@ export function PersonalInformation() { {t('settingForm.editButton')} )} - {getErrorMessage(saveProfileError) && ( - - {getErrorMessage(saveProfileError)} - - )} } > @@ -186,13 +174,6 @@ export function PersonalInformation() { > - ) : fetchProfileError ? ( - - - {getErrorMessage(fetchProfileError) || - t('settingForm.errorFetch')} - - ) : ( ( 'default', ); const [isVerified, setIsVerified] = useState(false); const [phones, setPhones] = useState([]); const [countryCode, setCountryCode] = useState('+98'); - const [formError, setFormError] = useState(); - const [touched, setTouched] = useState(false); + const [phoneNumberError, setPhoneNumberError] = useState(); + const [verificationCodeError, setVerificationCodeError] = useState(); + const [phoneNumberTouched, setPhoneNumberTouched] = useState(false); + const [verificationCodeTouched, setVerificationCodeTouched] = + useState(false); - const textFieldRef = useRef(null); - const inputRef = useRef(null); + const { isLoadingProfile, refetchProfile } = useProfile(); - const { - data: profileData, - loading: isLoading, - error: fetchError, - } = useApi(fetchProfile, { immediate: true }); + const { loading: isSendingCode, execute: executeSendCode } = + useApi(sendVerificationCode); - const { - data: sendCodeData, - loading: isSendingCode, - error: sendCodeError, - execute: executeSendCode, - } = useApi(sendVerificationCode); - - const { - data: confirmData, - loading: isVerifying, - error: confirmError, - execute: executeConfirmCode, - } = useApi(confirmPhoneNumberCode); - - const { - data: changePhoneData, - loading: isChangingPhone, - error: changePhoneError, - execute: executeChangePhone, - } = useApi(changePhoneNumber); - - useEffect(() => { - if (profileData?.success && profileData.phoneNumber) { - setPhones([ - { - phone: profileData.phoneNumber, - time: '', - withCode: profileData.phoneNumber, - }, - ]); - } - }, [profileData]); - - useEffect(() => { - if (fetchError) { - toast({ - message: - getErrorMessage(fetchError) || t('settingForm.errorFetchPhoneNumber'), - severity: 'error', - }); - } - }, [fetchError, toast, t]); - - useEffect(() => { - if (sendCodeData?.success) { - setButtonState('counting'); - setIsVerified(false); - toast({ - message: t('settingForm.codeSentSuccessfully'), - severity: 'success', - }); - } - }, [sendCodeData, t, toast]); - - useEffect(() => { - if (sendCodeError) { - toast({ - message: - getErrorMessage(sendCodeError) || t('settingForm.errorSendCode'), - severity: 'error', - }); - } - }, [sendCodeError, toast, t]); - - useEffect(() => { - if (confirmData?.success && confirmData.confirm) { - setIsVerified(true); - setShowToast(true); - const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, ''); - executeChangePhone({ phoneNumber: fullPhoneNumber }); - toast({ - message: t('settingForm.phoneVerified'), - severity: 'success', - }); - } - }, [confirmData, countryCode, phoneNumber, executeChangePhone, t, toast]); - - useEffect(() => { - if (confirmError) { - toast({ - message: - getErrorMessage(confirmError) || t('settingForm.errorConfirmCode'), - severity: 'error', - }); - } - }, [confirmError, toast, t]); - - useEffect(() => { - if (changePhoneData?.success) { - const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, ''); - setPhones([ - { - phone: phoneNumber, - time: t('settingForm.justNow'), - withCode: fullPhoneNumber, - }, - ]); - setIsEditing(false); - } - }, [changePhoneData, countryCode, phoneNumber, t]); - - useEffect(() => { - if (changePhoneError) { - toast({ - message: - getErrorMessage(changePhoneError) || - t('settingForm.errorChangePhone'), - severity: 'error', - }); - } - }, [changePhoneError, toast, t]); - - const apiError = useMemo( - () => sendCodeError || confirmError || changePhoneError, - [sendCodeError, confirmError, changePhoneError], + const { loading: isVerifying, execute: executeConfirmCode } = useApi( + confirmPhoneNumberCode, ); - const getErrorMessage = (error: unknown): string | undefined => { - if (!error) return undefined; - if (error instanceof Error) return error.message; - return String(error); - }; + const { loading: isChangingPhone, execute: executeChangePhone } = + useApi(changePhoneNumber); + + useEffect(() => { + const loadProfile = async () => { + const profileData = await refetchProfile(); + + if (!profileData) return; + + if (profileData?.success) { + setPhones([ + { + phone: profileData.phoneNumber, + time: '', + withCode: profileData.phoneNumber, + }, + ]); + } else { + toast({ + message: profileData.message, + severity: 'error', + }); + } + }; + loadProfile(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!phoneNumber) { + setPhoneNumberError(t('settingForm.thisFieldIsRequired')); + return; + } + if (!isPhoneValid(countryCode, phoneNumber)) { + setPhoneNumberError(t('settingForm.phoneNumberIsInvalid')); + } else { + setPhoneNumberError(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [countryCode, phoneNumber, phoneNumberTouched]); + + useEffect(() => { + 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(); }; - const handleBlur = () => { - setTouched(true); - if (!phoneNumber) { - toast({ - message: t('settingForm.phoneNumberIsInvalid'), - severity: 'error', - }); - setFormError(t('settingForm.thisFieldIsRequired')); - return; - } - if (!isPhoneValid(countryCode, phoneNumber)) { - setFormError(t('settingForm.phoneNumberIsInvalid')); + const handleBlur = (key: string) => { + if (key === 'phoneNumber') { + setPhoneNumberTouched(true); } else { - setFormError(undefined); + setVerificationCodeTouched(true); } }; @@ -195,36 +115,91 @@ export function PhoneNumber() { setVerificationCode(''); setIsVerified(false); setButtonState('default'); - setShowToast(false); - setFormError(undefined); - setTouched(false); + setPhoneNumberError(undefined); + setPhoneNumberTouched(false); + setVerificationCodeError(undefined); + setVerificationCodeTouched(false); } return !prev; }); }; const handleSendCode = async () => { - handleBlur(); - if (formError || !isPhoneValid(countryCode, phoneNumber)) return; - return executeSendCode({ + setPhoneNumberTouched(true); + if (phoneNumberError) return; + + const result = await executeSendCode({ phoneNumber: countryCode + phoneNumber.replace(/^0/, ''), }); + + if (!result) return; + + if (result?.success) { + setButtonState('counting'); + setIsVerified(false); + toast({ + message: result.message || t('settingForm.codeSentSuccessfully'), + severity: 'success', + }); + } else { + toast({ + message: result.message || t('message.serverError'), + severity: 'error', + }); + } }; - const handleVerifyCode = () => { - if (!verificationCode) { - setFormError(t('settingForm.verificationCodeRequired')); - return; - } - setFormError(undefined); - executeConfirmCode({ - phoneNumber: countryCode + phoneNumber.replace(/^0/, ''), + const handleVerifyCode = async () => { + setVerificationCodeTouched(true); + + if (verificationCodeError) return; + + const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, ''); + + const confirmResult = await executeConfirmCode({ + phoneNumber: fullPhoneNumber, verifyCode: verificationCode, }); - }; - const combinedError = formError || getErrorMessage(apiError); - const inputError: boolean = touched && !!combinedError; + if (!confirmResult) return; + + if (!confirmResult.success || !confirmResult.confirm) { + toast({ + message: confirmResult?.message || t('message.serverError'), + severity: 'error', + }); + return; + } + + setIsVerified(true); + toast({ + message: t('settingForm.phoneVerified'), + severity: 'success', + }); + + const changeResult = await executeChangePhone({ + phoneNumber: fullPhoneNumber, + }); + + if (!changeResult) return; + + if (changeResult?.success) { + setPhones([ + { + phone: phoneNumber, + time: t('settingForm.justNow'), + withCode: fullPhoneNumber, + }, + ]); + setIsEditing(false); + refetchProfile({ force: true }); + } else { + toast({ + message: changeResult?.message || t('message.serverError'), + severity: 'error', + }); + } + }; return ( @@ -240,7 +215,7 @@ export function PhoneNumber() { /> } > - {isLoading ? ( + {isLoadingProfile ? ( - ) : fetchError ? ( - - - {getErrorMessage(fetchError) || - t('settingForm.errorFetchPhoneNumber')} - - ) : isEditing ? ( } - inputRef={inputRef as React.RefObject} phones={phones} - showToast={showToast} - setShowToast={setShowToast} - t={t} /> ) : ( ([]); const [formError, setFormError] = useState(null); - const showToast = useToast(); + const toast = useToast(); - const { - data: profileData, - loading: isFetching, - error: fetchError, - } = useApi(fetchProfile, { immediate: true }); + const { isLoadingProfile, refetchProfile } = useProfile(); - const { - data: sendCodeData, - loading: isSendingCode, - error: sendCodeError, - execute: executeSendCode, - } = useApi(sendEmailCode); + const { loading: isSendingCode, execute: executeSendCode } = + useApi(sendEmailCode); - const { - data: confirmData, - loading: isConfirming, - error: confirmError, - execute: executeConfirmCode, - } = useApi(confirmEmailCode); + const { loading: isConfirming, execute: executeConfirmCode } = + useApi(confirmEmailCode); - const { - data: changeEmailData, - loading: isChangingEmail, - error: changeEmailError, - execute: executeChangeEmail, - } = useApi(changeEmail); + const { loading: isChangingEmail, execute: executeChangeEmail } = + useApi(changeEmail); const fullScreen = useMediaQuery((theme: Theme) => theme?.breakpoints.down('sm'), ); - const downMd = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')); + const downMd = useMediaQuery((theme: Theme) => theme?.breakpoints.down('md')); const computedMaxWidth = (fullScreen ? 'xs' : downMd ? 'sm' : 'md') as | 'xs' | 'sm' @@ -70,76 +54,34 @@ export function SocialMedia() { | 'xl'; useEffect(() => { - if (profileData?.success && profileData.email) { - const { email } = profileData; - setEmailList([ - { - email, - provider: email.includes('@gmail.') ? 'google' : 'email', - time: '', - }, - ]); - } - }, [profileData]); + const loadProfile = async () => { + const profileData = await refetchProfile(); - useEffect(() => { - if (sendCodeData?.success) { - showToast({ - message: t('settingForm.verificationCodeSent'), - severity: 'success', - }); - setDialogStep('enterCode'); - } - }, [sendCodeData, t, showToast]); + if (!profileData) return; - useEffect(() => { - if (confirmData?.success && confirmData.confirm) { - showToast({ - message: t('settingForm.verificationSuccessful'), - severity: 'success', - }); - executeChangeEmail({ email: emailInput }); - } - }, [confirmData, emailInput, executeChangeEmail, t, showToast]); + if (profileData?.success) { + const { email } = profileData; - useEffect(() => { - if (changeEmailData?.success) { - setEmailList((prev) => [ - ...prev, - { - email: emailInput, - provider: emailInput.includes('@gmail.') ? 'google' : 'email', - time: t('settingForm.justNow'), - }, - ]); - showToast({ - message: t('settingForm.emailChangedSuccessfully'), - severity: 'success', - }); - resetDialog(); - } - }, [changeEmailData, emailInput, t, showToast]); + if (!email) return; - useEffect(() => { - if (sendCodeError) { - showToast({ - message: getErrorMessage(sendCodeError) || t('settingForm.error'), - severity: 'error', - }); - } - if (confirmError) { - showToast({ - message: getErrorMessage(confirmError) || t('settingForm.error'), - severity: 'error', - }); - } - if (changeEmailError) { - showToast({ - message: getErrorMessage(changeEmailError) || t('settingForm.error'), - severity: 'error', - }); - } - }, [sendCodeError, confirmError, changeEmailError, t, showToast]); + setEmailList([ + { + email, + provider: email.includes('@gmail.') ? 'google' : 'email', + time: '', + }, + ]); + } else { + toast({ + message: profileData.message, + severity: 'error', + }); + } + }; + + loadProfile(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const resetDialog = () => { setOpenDialog(false); @@ -149,34 +91,74 @@ export function SocialMedia() { setDialogStep('enterEmail'); }; - const handleSendCode = () => { + 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); - executeSendCode({ email: emailInput }); + const sendCodeData = await executeSendCode({ email: emailInput }); + + if (!sendCodeData) return; + + if (sendCodeData.success) { + toast({ + message: sendCodeData.message || t('settingForm.verificationCodeSent'), + severity: 'success', + }); + setDialogStep('enterCode'); + } }; - const handleConfirmAndChangeEmail = () => { + const handleConfirmAndChangeEmail = async () => { if (verificationCode.length < 4) { setFormError(t('settingForm.verificationCodeRequired')); return; } setFormError(null); - executeConfirmCode({ email: emailInput, verifyCode: verificationCode }); - }; + const confirmData = await executeConfirmCode({ + email: emailInput, + verifyCode: verificationCode, + }); - const getErrorMessage = (error: unknown): string | null => { - if (!error) return null; - if (error instanceof Error) return error.message; - return String(error); - }; + if (!confirmData) return; - const apiError = useMemo( - () => getErrorMessage(sendCodeError || confirmError || changeEmailError), - [sendCodeError, confirmError, changeEmailError], - ); + if (!confirmData.success || !confirmData.confirm) { + toast({ + message: confirmData.message || t('settingForm.error'), + severity: 'error', + }); + + return; + } + + toast({ + message: confirmData.message || t('settingForm.verificationSuccessful'), + severity: 'success', + }); + + const changeEmailData = await executeChangeEmail({ email: emailInput }); + + if (!changeEmailData) return; + + if (changeEmailData.success) { + setEmailList((prev) => [ + ...prev, + { + email: emailInput, + provider: emailInput.includes('@gmail.') ? 'google' : 'email', + time: t('settingForm.justNow'), + }, + ]); + toast({ + message: t('settingForm.emailChangedSuccessfully'), + severity: 'success', + }); + resetDialog(); + + refetchProfile({ force: true }); + } + }; return ( @@ -187,7 +169,7 @@ export function SocialMedia() { setOpenDialog(true)} /> } > - {isFetching ? ( + {isLoadingProfile ? ( - ) : fetchError ? ( - - - {getErrorMessage(fetchError)} - - ) : ( )} @@ -216,7 +192,7 @@ export function SocialMedia() { setEmailInput={setEmailInput} verificationCode={verificationCode} setVerificationCode={setVerificationCode} - apiError={formError || apiError} + apiError={formError} isLoading={isSendingCode || isConfirming || isChangingEmail} dialogStep={dialogStep} onSendCode={handleSendCode} diff --git a/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx b/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx index 5bf9d27..c359332 100644 --- a/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx +++ b/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx @@ -110,11 +110,11 @@ export const InfoRowEdit = forwardRef( error={touched.gender && data.gender === Gender.None} > - {t('settingForm.genderPlaceholder', { ns: 'setting' })} + {t('settingForm.gender', { ns: 'setting' })}