304 lines
8.6 KiB
TypeScript
304 lines
8.6 KiB
TypeScript
import { useState, useRef, useEffect, useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import parsePhoneNumberFromString from 'libphonenumber-js';
|
|
import { PageWrapper } from '../PageWrapper';
|
|
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 { 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';
|
|
|
|
export function PhoneNumber() {
|
|
const { t, i18n } = useTranslation('setting');
|
|
const toast = useToast();
|
|
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 textFieldRef = useRef<HTMLDivElement>(null);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const {
|
|
data: profileData,
|
|
loading: isLoading,
|
|
error: fetchError,
|
|
} = useApi(fetchProfile, { immediate: true });
|
|
|
|
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);
|
|
toast({
|
|
message: t('settingForm.phoneChangedSuccessfully'),
|
|
severity: 'success',
|
|
});
|
|
}
|
|
}, [changePhoneData, countryCode, phoneNumber, t, toast]);
|
|
|
|
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 getErrorMessage = (error: unknown): string | undefined => {
|
|
if (!error) return undefined;
|
|
if (error instanceof Error) return error.message;
|
|
return String(error);
|
|
};
|
|
|
|
const isPhoneValid = (code: string, phone: string) => {
|
|
const phoneNum = parsePhoneNumberFromString(code + phone);
|
|
return phoneNum?.isValid();
|
|
};
|
|
|
|
const handleBlur = () => {
|
|
setTouched(true);
|
|
if (!phoneNumber) {
|
|
setFormError(t('settingForm.thisFieldIsRequired'));
|
|
return;
|
|
}
|
|
if (!isPhoneValid(countryCode, phoneNumber)) {
|
|
setFormError(t('settingForm.phoneNumberIsInvalid'));
|
|
} else {
|
|
setFormError(undefined);
|
|
}
|
|
};
|
|
|
|
const toggleEdit = () => {
|
|
setIsEditing((prev) => {
|
|
if (!prev) {
|
|
setPhoneNumber('');
|
|
setVerificationCode('');
|
|
setIsVerified(false);
|
|
setButtonState('default');
|
|
setShowToast(false);
|
|
setFormError(undefined);
|
|
setTouched(false);
|
|
}
|
|
return !prev;
|
|
});
|
|
};
|
|
|
|
const handleSendCode = () => {
|
|
handleBlur();
|
|
if (formError || !isPhoneValid(countryCode, phoneNumber)) return;
|
|
executeSendCode({
|
|
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
|
|
});
|
|
};
|
|
|
|
const handleVerifyCode = () => {
|
|
if (!verificationCode) {
|
|
setFormError(t('settingForm.verificationCodeRequired'));
|
|
return;
|
|
}
|
|
setFormError(undefined);
|
|
executeConfirmCode({
|
|
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
|
|
verifyCode: verificationCode,
|
|
});
|
|
};
|
|
|
|
const combinedError = formError || getErrorMessage(apiError);
|
|
const inputError: boolean = touched && !!combinedError;
|
|
|
|
return (
|
|
<PageWrapper>
|
|
<CardContainer
|
|
title={t('settingForm.titlePhoneNumber')}
|
|
subtitle={t('settingForm.descriptionPhoneNumber')}
|
|
highlighted={isEditing}
|
|
action={
|
|
<PhoneActionButtons
|
|
isEditing={isEditing}
|
|
toggleEdit={toggleEdit}
|
|
t={t}
|
|
/>
|
|
}
|
|
>
|
|
{isLoading ? (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
p: 4,
|
|
minHeight: '150px',
|
|
}}
|
|
>
|
|
<CircularProgress />
|
|
</Box>
|
|
) : fetchError ? (
|
|
<Box sx={{ textAlign: 'center', p: 4 }}>
|
|
<Typography color="error">
|
|
{getErrorMessage(fetchError) ||
|
|
t('settingForm.errorFetchPhoneNumber')}
|
|
</Typography>
|
|
</Box>
|
|
) : isEditing ? (
|
|
<PhoneEditForm
|
|
phoneNumber={phoneNumber}
|
|
setPhoneNumber={setPhoneNumber}
|
|
countryCode={countryCode}
|
|
setCountryCode={setCountryCode}
|
|
verificationCode={verificationCode}
|
|
setVerificationCode={setVerificationCode}
|
|
isVerified={isVerified}
|
|
isVerifying={isVerifying || isSendingCode || isChangingPhone}
|
|
buttonState={buttonState}
|
|
setButtonState={setButtonState}
|
|
handleSendCode={handleSendCode}
|
|
handleVerifyClick={handleVerifyCode}
|
|
error={combinedError}
|
|
inputError={inputError}
|
|
handleBlur={handleBlur}
|
|
textFieldRef={textFieldRef as React.RefObject<HTMLDivElement>}
|
|
inputRef={inputRef as React.RefObject<HTMLInputElement>}
|
|
phones={phones}
|
|
showToast={showToast}
|
|
setShowToast={setShowToast}
|
|
t={t}
|
|
/>
|
|
) : (
|
|
<PhoneDisplay
|
|
phones={phones.map((p) => {
|
|
let localPhone = p.withCode;
|
|
if (localPhone.startsWith('+98')) {
|
|
localPhone = '0' + localPhone.slice(3);
|
|
}
|
|
return {
|
|
...p,
|
|
phone: toLocaleDigits(localPhone, i18n.language),
|
|
};
|
|
})}
|
|
/>
|
|
)}
|
|
</CardContainer>
|
|
</PageWrapper>
|
|
);
|
|
}
|