Files
Account/src/features/profile/components/userInformation/PhoneNumber.tsx
2025-09-29 19:31:34 +03:30

276 lines
8.4 KiB
TypeScript

import { useState, 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 } from '@mui/material';
import { toLocaleDigits } from '@/utils/persianDigit';
import { useApi } from '@/hooks/useApi';
import {
sendVerificationCode,
confirmPhoneNumberCode,
changePhoneNumber,
} from '../../api/settingsApi';
import { type Phone } from '../../types/settingsType';
import { useToast } from '@rkheftan/harmony-ui';
import { useProfile } from '../../hooks/useProfile';
import type { CountryCode } from '@/types/commonTypes';
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 [buttonState, setButtonState] = useState<'default' | 'counting'>(
'default',
);
const [isVerified, setIsVerified] = useState(false);
const [isCodeSent, setIsCodeSent] = useState(false);
const [phones, setPhones] = useState<Phone[]>([]);
const [countryCode, setCountryCode] = useState<CountryCode>('+98');
const [phoneNumberError, setPhoneNumberError] = useState<string>();
const [verificationCodeError, setVerificationCodeError] = useState<string>();
const [phoneNumberTouched, setPhoneNumberTouched] = useState<boolean>(false);
const [verificationCodeTouched, setVerificationCodeTouched] =
useState<boolean>(false);
const { isLoadingProfile, refetchProfile } = useProfile();
const { loading: isSendingCode, execute: executeSendCode } =
useApi(sendVerificationCode);
const { loading: isVerifying, execute: executeConfirmCode } = useApi(
confirmPhoneNumberCode,
);
const { loading: isChangingPhone, execute: executeChangePhone } =
useApi(changePhoneNumber);
const isBusy = useMemo(
() => isVerifying || isSendingCode || isChangingPhone,
[isVerifying, isSendingCode, isChangingPhone],
);
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',
});
}
};
void loadProfile();
}, [refetchProfile, toast]);
useEffect(() => {
if (!phoneNumberTouched) return;
if (!phoneNumber) {
setPhoneNumberError(t('settingForm.thisFieldIsRequired'));
return;
}
const parsed = parsePhoneNumberFromString(countryCode + phoneNumber);
const valid = Boolean(parsed?.isValid());
if (!valid) {
setPhoneNumberError(t('settingForm.phoneNumberIsInvalid'));
} else {
setPhoneNumberError(undefined);
}
}, [countryCode, phoneNumber, phoneNumberTouched, t]);
useEffect(() => {
if (!verificationCodeTouched) return;
if (!verificationCode) {
setVerificationCodeError(t('settingForm.verificationCodeRequired'));
return;
}
setVerificationCodeError(undefined);
}, [verificationCode, verificationCodeTouched, t]);
const handleBlur = (key: string) => {
if (key === 'phoneNumber') {
setPhoneNumberTouched(true);
} else {
setVerificationCodeTouched(true);
}
};
const toggleEdit = () => {
setIsEditing((prev) => {
if (!prev) {
setPhoneNumber('');
setVerificationCode('');
setIsVerified(false);
setButtonState('default');
setPhoneNumberError(undefined);
setPhoneNumberTouched(false);
setVerificationCodeError(undefined);
setVerificationCodeTouched(false);
setIsCodeSent(false);
}
return !prev;
});
};
const handleSendCode = async () => {
setPhoneNumberTouched(true);
if (!phoneNumber || phoneNumberError) return;
const result = await executeSendCode({
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
});
if (!result) return;
if (result?.success) {
setButtonState('counting');
setIsVerified(false);
setIsCodeSent(true);
toast({
message: result.message || t('settingForm.codeSentSuccessfully'),
severity: 'success',
});
} else {
toast({
message: result.message || t('message.serverError'),
severity: 'error',
});
}
};
const handleVerifyCode = async () => {
setVerificationCodeTouched(true);
if (!verificationCode || verificationCodeError) return;
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
const confirmResult = await executeConfirmCode({
phoneNumber: fullPhoneNumber,
verifyCode: verificationCode,
});
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>
<CardContainer
title={t('settingForm.titlePhoneNumber')}
subtitle={t('settingForm.descriptionPhoneNumber')}
highlighted={isEditing}
action={
<PhoneActionButtons
isEditing={isEditing}
toggleEdit={toggleEdit}
t={t}
formId="phoneForm"
isSubmitting={isBusy}
/>
}
>
{isLoadingProfile ? (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
p: 4,
minHeight: '150px',
}}
>
<CircularProgress />
</Box>
) : isEditing ? (
<Box
component="form"
id="phoneForm"
noValidate
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
if (!isBusy) void handleVerifyCode();
}}
>
<PhoneEditForm
phoneNumber={phoneNumber}
setPhoneNumber={setPhoneNumber}
countryCode={countryCode}
setCountryCode={setCountryCode}
verificationCode={verificationCode}
setVerificationCode={setVerificationCode}
isVerified={isVerified}
isVerifying={isBusy}
isSendingCode={isSendingCode}
isCodeSent={isCodeSent}
setIsCodeSent={setIsCodeSent}
buttonState={buttonState}
setButtonState={setButtonState}
handleSendCode={handleSendCode}
handleVerifyClick={handleVerifyCode}
phoneNumberError={
phoneNumberTouched ? phoneNumberError : undefined
}
verificationCodeError={
verificationCodeTouched ? verificationCodeError : undefined
}
handleBlur={handleBlur}
phones={phones}
/>
</Box>
) : (
<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>
);
}