fix: dashboard api calls, multi profile fetch

This commit is contained in:
Sajad Mirjalili
2025-08-21 14:51:56 +03:30
parent 4d84a29bda
commit 16fa6615c9
24 changed files with 656 additions and 679 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useRef, useEffect, useMemo } from 'react';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import parsePhoneNumberFromString from 'libphonenumber-js';
import { PageWrapper } from '../PageWrapper';
@@ -6,17 +6,17 @@ import { CardContainer } from '@/components/CardContainer';
import PhoneDisplay from './phoneNumber/PhoneDisplay';
import PhoneEditForm from './phoneNumber/PhoneEditForm';
import PhoneActionButtons from './phoneNumber/PhoneActionButtons';
import { CircularProgress, Box, Typography } from '@mui/material';
import { CircularProgress, Box } from '@mui/material';
import { toLocaleDigits } from '@/utils/persianDigit';
import { useApi } from '@/hooks/useApi';
import {
fetchProfile,
sendVerificationCode,
confirmPhoneNumberCode,
changePhoneNumber,
} from '../../api/settingsApi';
import { type Phone } from '../../types/settingsType';
import { useToast } from '@rkheftan/harmony-ui';
import { useProfile } from '../../hooks/useProfile';
export function PhoneNumber() {
const { t, i18n } = useTranslation('setting');
@@ -24,167 +24,87 @@ export function PhoneNumber() {
const [isEditing, setIsEditing] = useState(false);
const [phoneNumber, setPhoneNumber] = useState('');
const [verificationCode, setVerificationCode] = useState('');
const [showToast, setShowToast] = useState(false);
const [buttonState, setButtonState] = useState<'default' | 'counting'>(
'default',
);
const [isVerified, setIsVerified] = useState(false);
const [phones, setPhones] = useState<Phone[]>([]);
const [countryCode, setCountryCode] = useState('+98');
const [formError, setFormError] = useState<string>();
const [touched, setTouched] = useState<boolean>(false);
const [phoneNumberError, setPhoneNumberError] = useState<string>();
const [verificationCodeError, setVerificationCodeError] = useState<string>();
const [phoneNumberTouched, setPhoneNumberTouched] = useState<boolean>(false);
const [verificationCodeTouched, setVerificationCodeTouched] =
useState<boolean>(false);
const textFieldRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(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 (
<PageWrapper>
@@ -240,7 +215,7 @@ export function PhoneNumber() {
/>
}
>
{isLoading ? (
{isLoadingProfile ? (
<Box
sx={{
display: 'flex',
@@ -252,13 +227,6 @@ export function PhoneNumber() {
>
<CircularProgress />
</Box>
) : fetchError ? (
<Box sx={{ textAlign: 'center', p: 4 }}>
<Typography color="error">
{getErrorMessage(fetchError) ||
t('settingForm.errorFetchPhoneNumber')}
</Typography>
</Box>
) : isEditing ? (
<PhoneEditForm
phoneNumber={phoneNumber}
@@ -269,19 +237,17 @@ export function PhoneNumber() {
setVerificationCode={setVerificationCode}
isVerified={isVerified}
isVerifying={isVerifying || isSendingCode || isChangingPhone}
isSendingCode={isSendingCode}
buttonState={buttonState}
setButtonState={setButtonState}
handleSendCode={handleSendCode}
handleVerifyClick={handleVerifyCode}
error={combinedError}
inputError={inputError}
phoneNumberError={phoneNumberTouched ? phoneNumberError : undefined}
verificationCodeError={
verificationCodeTouched ? verificationCodeError : undefined
}
handleBlur={handleBlur}
textFieldRef={textFieldRef as React.RefObject<HTMLDivElement>}
inputRef={inputRef as React.RefObject<HTMLInputElement>}
phones={phones}
showToast={showToast}
setShowToast={setShowToast}
t={t}
/>
) : (
<PhoneDisplay