chore: move api to another component

This commit is contained in:
Koosha Lahouti
2025-08-14 09:06:53 +03:30
parent d42913bced
commit 5a72b597c8
9 changed files with 390 additions and 372 deletions

26
package-lock.json generated
View File

@@ -1361,32 +1361,6 @@
"url": "https://opencollective.com/mui-org" "url": "https://opencollective.com/mui-org"
} }
}, },
"node_modules/@mui/icons-material": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.1.tgz",
"integrity": "sha512-upzCtG6awpL6noEZlJ5Z01khZ9VnLNLaj7tb6iPbN6G97eYfUTs8e9OyPKy3rEms3VQWmVBfri7jzeaRxdFIzA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.2"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^7.3.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": { "node_modules/@mui/material": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.1.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.1.tgz",

View File

@@ -0,0 +1,122 @@
import axios, { isAxiosError } from 'axios';
import apiClient from '@/lib/apiClient';
import {
type GetProfileApiResponse,
type SaveProfileApiResponse,
type TokenApiResponse,
type PhoneNumberApiResponse,
type ConfirmPhoneNumberApiResponse,
} from '../types/settingsApiType';
import { type InfoRowData } from '../types/settingsType';
const storedToken = () => localStorage.getItem('authToken');
export async function fetchProfile(): Promise<{ data: GetProfileApiResponse }> {
const res = await apiClient.post<GetProfileApiResponse>(
'/Profile/GetProfile',
{},
{ headers: { Authorization: `Bearer ${storedToken()}` } },
);
return { data: res.data };
}
export async function saveProfile(payload?: {
data: InfoRowData;
imageUrl: string | null;
}): Promise<{ data: SaveProfileApiResponse }> {
if (!payload) {
throw new Error('Payload for saving profile is missing.');
}
const res = await apiClient.post<SaveProfileApiResponse>(
'/Profile/SaveProfilePersonalInforamtion',
payload,
{ headers: { Authorization: `Bearer ${storedToken()}` } },
);
return { data: res.data };
}
export async function getToken(): Promise<{
data: { success: boolean; message?: string };
}> {
const apiUrl = 'https://accounts.business-harmony.com';
const tokenEndpoint = `${apiUrl}/connect/token`;
try {
const body = new URLSearchParams();
body.set('grant_type', 'password');
body.set('username', 'zareian.1381@gmail.com');
body.set('password', '123@Qweasd');
body.set('client_id', 'harmony_identity');
body.set('scope', 'openid harmony_identity profile offline_access');
const response = await axios.post<TokenApiResponse>(
tokenEndpoint,
body.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
if (response.data?.access_token) {
localStorage.setItem('authToken', response.data.access_token);
return { data: { success: true } };
} else {
throw new Error('No access token in response');
}
} catch (error: unknown) {
let message = 'Failed to get token';
if (isAxiosError(error) && error.response) {
message = `Request failed with status ${error.response.status}`;
} else if (error instanceof Error) {
message = error.message;
}
throw new Error(message);
}
}
export async function sendVerificationCode(payload?: {
phoneNumber: string;
}): Promise<{ data: PhoneNumberApiResponse }> {
if (!payload) {
throw new Error('Payload for sending verification code is missing.');
}
const res = await apiClient.post<PhoneNumberApiResponse>(
'/Profile/SendVerfiyPhoneNumberCode',
payload,
{ headers: { Authorization: `Bearer ${storedToken()}` } },
);
return { data: res.data };
}
export async function confirmPhoneNumberCode(payload?: {
phoneNumber: string;
verifyCode: string;
}): Promise<{ data: ConfirmPhoneNumberApiResponse }> {
if (!payload) {
throw new Error('Payload for confirming phone number is missing.');
}
const res = await apiClient.post<ConfirmPhoneNumberApiResponse>(
'/Profile/ConfirmPhoneNumberChangeCode',
payload,
{ headers: { Authorization: `Bearer ${storedToken()}` } },
);
return { data: res.data };
}
export async function changePhoneNumber(payload?: {
phoneNumber: string;
}): Promise<{ data: PhoneNumberApiResponse }> {
if (!payload) {
throw new Error('Payload for changing phone number is missing.');
}
const res = await apiClient.post<PhoneNumberApiResponse>(
'/Profile/ChangePhoneNumber',
payload,
{ headers: { Authorization: `Bearer ${storedToken()}` } },
);
return { data: res.data };
}

View File

@@ -6,29 +6,9 @@ import { ProfileImage } from './personalInformation/ProfileImage';
import { InfoRowDisplay } from './personalInformation/InfoRowDisplay'; import { InfoRowDisplay } from './personalInformation/InfoRowDisplay';
import { InfoRowEdit } from './personalInformation/InfoRowEdit'; import { InfoRowEdit } from './personalInformation/InfoRowEdit';
import { PageWrapper } from '../PageWrapper'; import { PageWrapper } from '../PageWrapper';
import { Gender, type InfoRowData } from '../../types'; import { useManualApi } from '@/hooks/useApi';
import axios, { isAxiosError } from 'axios'; import { Gender, type InfoRowData } from '../../types/settingsType';
import apiClient from '@/lib/apiClient'; import { fetchProfile, saveProfile, getToken } from '../../api/settingsApi';
interface GetProfileApiResponse {
success: boolean;
message?: string;
firstName?: string;
lastName?: string;
nationalCode?: string;
gender?: Gender;
countryCode?: string;
profileImageUrl?: string;
}
interface SaveProfileApiResponse {
success: boolean;
message?: string;
}
interface TokenApiResponse {
access_token: string;
}
export function PersonalInformation() { export function PersonalInformation() {
const { t } = useTranslation('setting'); const { t } = useTranslation('setting');
@@ -42,71 +22,60 @@ export function PersonalInformation() {
country: '', country: '',
}); });
const [originalData, setOriginalData] = useState<InfoRowData | null>(null); const [originalData, setOriginalData] = useState<InfoRowData | null>(null);
// const [token, setToken] = useState<string | null>(null);
const [tokenError, setTokenError] = useState<string | null>(null);
const [saveError, setSaveError] = useState<string | null>(null);
const storedToken = localStorage.getItem('authToken');
const [isLoading, setIsLoading] = useState(true); const {
const [fetchError, setFetchError] = useState<string | null>(null); data: profileData,
loading: isLoadingProfile,
error: fetchProfileError,
execute: executeFetchProfile,
} = useManualApi(fetchProfile);
const {
data: saveData,
loading: isSavingProfile,
error: saveProfileError,
execute: executeSaveProfile,
} = useManualApi(saveProfile);
const {
data: tokenData,
loading: isGettingToken,
error: tokenError,
execute: executeGetToken,
} = useManualApi(getToken);
useEffect(() => { useEffect(() => {
const fetchProfile = async () => { executeFetchProfile();
setIsLoading(true); }, []);
setFetchError(null);
try {
const res = await apiClient.post<GetProfileApiResponse>(
'/Profile/GetProfile',
{},
{
headers: {
Authorization: `Bearer ${storedToken}`,
},
},
);
if (res.data?.success) {
const profile = res.data;
const fetchedData = {
firstName: profile.firstName ?? '',
lastName: profile.lastName ?? '',
nationalCode: profile.nationalCode ?? '',
gender: Object.values(Gender).includes(profile.gender as Gender)
? (profile.gender as Gender)
: Gender.None,
country: profile.countryCode ?? '',
};
setData(fetchedData);
setOriginalData(fetchedData);
setUploadedImageUrl(profile.profileImageUrl || null);
} else {
throw new Error(res.data.message || t('settingForm.failRetrieve'));
}
} catch (error: unknown) {
let message = t('settingForm.errorFetch');
if (error instanceof Error) {
message = error.message;
}
setFetchError(message);
} finally {
setIsLoading(false);
}
};
if (storedToken) { useEffect(() => {
fetchProfile(); if (profileData?.success) {
} else { const fetchedData = {
setIsLoading(false); firstName: profileData.firstName ?? '',
setFetchError(t('settingForm.notLoggedIn')); 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 || null);
} }
}, [storedToken, t]); }, [profileData]);
const initials = `${data?.firstName?.trim()[0] || ''}${ useEffect(() => {
data?.lastName?.trim()[0] || '' if (saveData?.success) {
}`; setIsEditing(false);
setOriginalData(data);
}
}, [saveData, data]);
const initials = `${data?.firstName?.trim()[0] || ''}${data?.lastName?.trim()[0] || ''}`;
const handleEditClick = () => { const handleEditClick = () => {
setIsEditing(true); setIsEditing(true);
setSaveError(null);
setOriginalData(data); setOriginalData(data);
}; };
@@ -115,85 +84,21 @@ export function PersonalInformation() {
if (originalData) { if (originalData) {
setData(originalData); setData(originalData);
} }
setSaveError(null);
}; };
const handleSaveClick = async () => { const handleSaveClick = async () => {
if (!data) return; if (!data) return;
setSaveError(null); executeSaveProfile({ data, imageUrl: uploadedImageUrl });
try {
const formData = new FormData();
formData.append('FirstName', data.firstName || '');
formData.append('LastName', data.lastName || '');
formData.append('NationalCode', data.nationalCode || '');
formData.append('Gender', String(data.gender ?? Gender.None));
formData.append('CountryCode', data.country || '');
if (uploadedImageUrl && uploadedImageUrl.startsWith('data:')) {
const response = await fetch(uploadedImageUrl);
const blob = await response.blob();
formData.append('Image', blob, 'profile.jpg');
}
const res = await apiClient.post<SaveProfileApiResponse>(
'Profile/SaveProfilePersonalInforamtion',
formData,
{
headers: {
Authorization: `Bearer ${storedToken}`,
},
},
);
if (res.data.success) {
setIsEditing(false);
setOriginalData(data);
} else {
throw new Error(res.data.message || t('settingForm.unknownError'));
}
} catch (error: unknown) {
let message = t('settingForm.checkConnection');
if (error instanceof Error) {
message = error.message;
}
setSaveError(message);
}
}; };
const apiUrl = 'https://accounts.business-harmony.com'; const handleGetTokenClick = async () => {
const tokenEndpoint = `${apiUrl}/connect/token`; executeGetToken();
const getToken = async () => { };
setTokenError(null);
try { const getErrorMessage = (error: unknown): string | null => {
const body = new URLSearchParams(); if (!error) return null;
body.set('grant_type', 'password'); if (error instanceof Error) return error.message;
body.set('username', 'zareian.1381@gmail.com'); return String(error);
body.set('password', '123@Qweasd');
body.set('client_id', 'harmony_identity');
body.set('scope', 'openid harmony_identity profile offline_access');
const response = await axios.post<TokenApiResponse>(
tokenEndpoint,
body.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
if (response.data?.access_token) {
localStorage.setItem('authToken', response.data.access_token);
} else {
throw new Error('No access token in response');
}
} catch (error: unknown) {
let message = 'Failed to get token';
if (isAxiosError(error) && error.response) {
message = `Request failed with status ${error.response.status}`;
} else if (error instanceof Error) {
message = error.message;
}
setTokenError(message);
}
}; };
return ( return (
@@ -222,12 +127,12 @@ export function PersonalInformation() {
<Button <Button
variant="contained" variant="contained"
color="secondary" color="secondary"
onClick={getToken} onClick={handleGetTokenClick}
size="large" size="large"
sx={{ textTransform: 'none' }} sx={{ textTransform: 'none' }}
disabled={isLoading} disabled={isLoadingProfile || isGettingToken || isSavingProfile}
> >
Get Token {isGettingToken ? <CircularProgress size={24} /> : 'Get Token'}
</Button> </Button>
{isEditing ? ( {isEditing ? (
<> <>
@@ -240,6 +145,7 @@ export function PersonalInformation() {
textTransform: 'none', textTransform: 'none',
width: { xs: '100%', sm: 'auto' }, width: { xs: '100%', sm: 'auto' },
}} }}
disabled={isSavingProfile}
> >
{t('settingForm.rejectButton')} {t('settingForm.rejectButton')}
</Button> </Button>
@@ -251,8 +157,13 @@ export function PersonalInformation() {
textTransform: 'none', textTransform: 'none',
width: { xs: '100%', sm: 'auto' }, width: { xs: '100%', sm: 'auto' },
}} }}
disabled={isSavingProfile}
> >
{t('settingForm.saveButton')} {isSavingProfile ? (
<CircularProgress size={24} color="inherit" />
) : (
t('settingForm.saveButton')
)}
</Button> </Button>
</> </>
) : ( ) : (
@@ -260,27 +171,33 @@ export function PersonalInformation() {
onClick={handleEditClick} onClick={handleEditClick}
size="large" size="large"
variant="outlined" variant="outlined"
sx={{ sx={{ borderRadius: 1 }}
borderRadius: 1, disabled={isLoadingProfile}
}}
disabled={isLoading}
> >
{t('settingForm.editButton')} {t('settingForm.editButton')}
</Button> </Button>
)} )}
</Box> </Box>
{saveError && ( {getErrorMessage(saveProfileError) && (
<Typography <Typography
color="error" color="error"
sx={{ mt: 1, textAlign: 'right', width: '100%' }} sx={{ mt: 1, textAlign: 'right', width: '100%' }}
> >
{saveError} {getErrorMessage(saveProfileError)}
</Typography>
)}
{tokenData?.success && (
<Typography
color="green"
sx={{ mt: 1, textAlign: 'right', width: '100%' }}
>
Token fetched and stored successfully!
</Typography> </Typography>
)} )}
</Box> </Box>
} }
> >
{isLoading ? ( {isLoadingProfile ? (
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -292,14 +209,19 @@ export function PersonalInformation() {
> >
<CircularProgress /> <CircularProgress />
</Box> </Box>
) : fetchError ? ( ) : fetchProfileError ? (
<Box sx={{ textAlign: 'center', p: 4 }}> <Box sx={{ textAlign: 'center', p: 4 }}>
<Typography color="error">{fetchError}</Typography> <Typography color="error">
{getErrorMessage(fetchProfileError) ||
t('settingForm.errorFetch')}
</Typography>
</Box> </Box>
) : ( ) : (
<> <>
{tokenError && ( {getErrorMessage(tokenError) && (
<Box sx={{ mt: 2, color: 'red' }}>Error: {tokenError}</Box> <Box sx={{ p: 2, mx: { xs: 2, sm: 3, md: 4 }, color: 'red' }}>
Error: {getErrorMessage(tokenError)}
</Box>
)} )}
<Box <Box
sx={{ sx={{

View File

@@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import parsePhoneNumberFromString from 'libphonenumber-js'; import parsePhoneNumberFromString from 'libphonenumber-js';
import { PageWrapper } from '../PageWrapper'; import { PageWrapper } from '../PageWrapper';
@@ -6,9 +6,15 @@ import { CardContainer } from '@/components/CardContainer';
import PhoneDisplay from './phoneNumber/PhoneDisplay'; import PhoneDisplay from './phoneNumber/PhoneDisplay';
import PhoneEditForm from './phoneNumber/PhoneEditForm'; import PhoneEditForm from './phoneNumber/PhoneEditForm';
import PhoneActionButtons from './phoneNumber/PhoneActionButtons'; import PhoneActionButtons from './phoneNumber/PhoneActionButtons';
import apiClient from '@/lib/apiClient';
import { CircularProgress, Box, Typography } from '@mui/material'; import { CircularProgress, Box, Typography } from '@mui/material';
import { toLocaleDigits } from '@/utils/persianDigit'; import { toLocaleDigits } from '@/utils/persianDigit';
import { useManualApi } from '@/hooks/useApi';
import {
fetchProfile,
sendVerificationCode,
confirmPhoneNumberCode,
changePhoneNumber,
} from '../../api/settingsApi';
interface Phone { interface Phone {
phone: string; phone: string;
@@ -16,21 +22,6 @@ interface Phone {
withCode: string; withCode: string;
} }
interface GetProfileApiResponse {
success: boolean;
message?: string;
phoneNumber?: string;
}
interface ApiResponse {
success: boolean;
message?: string;
}
interface ConfirmApiResponse extends ApiResponse {
confirm?: boolean;
}
export function PhoneNumber() { export function PhoneNumber() {
const { t, i18n } = useTranslation('setting'); const { t, i18n } = useTranslation('setting');
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
@@ -40,185 +31,158 @@ export function PhoneNumber() {
const [buttonState, setButtonState] = useState<'default' | 'counting'>( const [buttonState, setButtonState] = useState<'default' | 'counting'>(
'default', 'default',
); );
const [isVerifying, setIsVerifying] = useState(false);
const [isVerified, setIsVerified] = useState(false); const [isVerified, setIsVerified] = useState(false);
const [phones, setPhone] = useState<Phone[]>([]); const [phones, setPhones] = useState<Phone[]>([]);
const [countryCode, setCountryCode] = useState('+98'); const [countryCode, setCountryCode] = useState('+98');
const [formError, setFormError] = useState<string>();
const [touched, setTouched] = useState<boolean>(false);
const textFieldRef = useRef<HTMLDivElement>(null); const textFieldRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [error, setError] = useState<string>();
const [touched, setTouched] = useState<boolean>(false);
const inputError: boolean = touched && !!error;
const token = localStorage.getItem('authToken');
const [isLoading, setIsLoading] = useState(true); const {
const [fetchError, setFetchError] = useState<string | null>(null); data: profileData,
loading: isLoading,
error: fetchError,
execute: executeFetchProfile,
} = useManualApi(fetchProfile);
const {
data: sendCodeData,
loading: isSendingCode,
error: sendCodeError,
execute: executeSendCode,
} = useManualApi(sendVerificationCode);
const {
data: confirmData,
loading: isVerifying,
error: confirmError,
execute: executeConfirmCode,
} = useManualApi(confirmPhoneNumberCode);
const {
data: changePhoneData,
loading: isChangingPhone,
error: changePhoneError,
execute: executeChangePhone,
} = useManualApi(changePhoneNumber);
useEffect(() => { useEffect(() => {
const fetchPhoneNumber = async () => { if (!isEditing) {
setIsLoading(true); executeFetchProfile();
setFetchError(null); }
if (!token) { }, [isEditing]);
setIsLoading(false);
setFetchError(t('settingForm.notLoggedIn'));
return;
}
try {
const res = await apiClient.post<GetProfileApiResponse>(
'/Profile/GetProfile',
{},
{ headers: { Authorization: `Bearer ${token}` } },
);
if (res.data.success && res.data.phoneNumber) { useEffect(() => {
setPhone([ if (profileData?.success && profileData.phoneNumber) {
{ setPhones([
phone: res.data.phoneNumber, {
time: '', phone: profileData.phoneNumber,
withCode: res.data.phoneNumber, time: '',
}, withCode: profileData.phoneNumber,
]); },
} else if (!res.data.success) { ]);
throw new Error( }
res.data.message || t('settingForm.failFetchPhoneNumber'), }, [profileData]);
);
}
} catch (err: unknown) {
let message = t('settingForm.errorFetchPhoneNumber');
if (err instanceof Error) {
message = err.message;
}
setFetchError(message);
} finally {
setIsLoading(false);
}
};
fetchPhoneNumber(); useEffect(() => {
}, [token, t]); if (sendCodeData?.success) {
setButtonState('counting');
setIsVerified(false);
}
}, [sendCodeData]);
useEffect(() => {
if (confirmData?.success && confirmData.confirm) {
setIsVerified(true);
setShowToast(true);
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
executeChangePhone({ phoneNumber: fullPhoneNumber });
}
}, [confirmData, countryCode, phoneNumber, executeChangePhone]);
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]);
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 isPhoneValid = (code: string, phone: string) => {
const phoneNum = parsePhoneNumberFromString(code + phone); const phoneNum = parsePhoneNumberFromString(code + phone);
return phoneNum && phoneNum.isValid(); return phoneNum?.isValid();
}; };
const handleBlur = () => { const handleBlur = () => {
setTouched(true); setTouched(true);
if (!phoneNumber) { if (!phoneNumber) {
setError(t('settingForm.thisFieldIsRequired')); setFormError(t('settingForm.thisFieldIsRequired'));
return; return;
} }
if (!isPhoneValid(countryCode, phoneNumber)) { if (!isPhoneValid(countryCode, phoneNumber)) {
setError(t('settingForm.phoneNumberIsInvalid')); setFormError(t('settingForm.phoneNumberIsInvalid'));
} else { } else {
setError(undefined); setFormError(undefined);
} }
}; };
const toggleEdit = () => { const toggleEdit = () => {
setIsEditing((prev) => { setIsEditing((prev) => {
const enteringEditMode = !prev; if (!prev) {
if (enteringEditMode) {
setPhoneNumber(''); setPhoneNumber('');
setVerificationCode(''); setVerificationCode('');
setIsVerified(false); setIsVerified(false);
setButtonState('default'); setButtonState('default');
setShowToast(false); setShowToast(false);
setError(undefined); setFormError(undefined);
setTouched(false); setTouched(false);
} }
return enteringEditMode; return !prev;
}); });
}; };
const handleSendCode = async () => { const handleSendCode = () => {
if (!phoneNumber) return; handleBlur();
if (!isPhoneValid(countryCode, phoneNumber)) { if (formError || !isPhoneValid(countryCode, phoneNumber)) return;
setError(t('settingForm.phoneNumberIsInvalid'));
return;
}
setError(undefined);
try { executeSendCode({
const res = await apiClient.post<ApiResponse>( phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
'/Profile/SendVerfiyPhoneNumberCode', });
{
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
},
{ headers: { Authorization: `Bearer ${token}` } },
);
if (res.data.success) {
setButtonState('counting');
setIsVerified(false);
} else {
setError(res.data.message || t('settingForm.sendCodeFailed'));
}
} catch (error: unknown) {
setError(t('settingForm.sendCodeFailed'));
}
}; };
const handleVerifyCode = async () => { const handleVerifyCode = () => {
if (!verificationCode) { if (!verificationCode) {
setError(t('settingForm.verificationCodeRequired')); setFormError(t('settingForm.verificationCodeRequired'));
return; return;
} }
setIsVerifying(true); setFormError(undefined);
setError(undefined); executeConfirmCode({
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
try { verifyCode: verificationCode,
const res = await apiClient.post<ConfirmApiResponse>( });
'/Profile/ConfirmPhoneNumberChangeCode',
{
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
verifyCode: verificationCode,
},
{ headers: { Authorization: `Bearer ${token}` } },
);
if (res.data.success && res.data.confirm) {
setIsVerified(true);
setShowToast(true);
await handleChangePhoneNumber();
} else {
setError(res.data.message || t('settingForm.verifyCodeFailed'));
setIsVerified(false);
}
} catch (error: unknown) {
setError(t('settingForm.verifyCodeFailed'));
setIsVerified(false);
} finally {
setIsVerifying(false);
}
}; };
const handleChangePhoneNumber = async () => { const combinedError = formError || getErrorMessage(apiError);
try { const inputError: boolean = touched && !!combinedError;
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
const res = await apiClient.post<ApiResponse>(
'/Profile/ChangePhoneNumber',
{
phoneNumber: fullPhoneNumber,
},
{ headers: { Authorization: `Bearer ${token}` } },
);
if (res.data.success) {
setPhone([
{
phone: phoneNumber,
time: t('settingForm.justNow'),
withCode: fullPhoneNumber,
},
]);
setIsEditing(false);
} else {
setError(res.data.message || t('settingForm.changePhoneFailed'));
}
} catch (error: unknown) {
setError(t('settingForm.changePhoneFailed'));
}
};
return ( return (
<PageWrapper> <PageWrapper>
@@ -248,7 +212,10 @@ export function PhoneNumber() {
</Box> </Box>
) : fetchError ? ( ) : fetchError ? (
<Box sx={{ textAlign: 'center', p: 4 }}> <Box sx={{ textAlign: 'center', p: 4 }}>
<Typography color="error">{fetchError}</Typography> <Typography color="error">
{getErrorMessage(fetchError) ||
t('settingForm.errorFetchPhoneNumber')}
</Typography>
</Box> </Box>
) : isEditing ? ( ) : isEditing ? (
<PhoneEditForm <PhoneEditForm
@@ -259,12 +226,12 @@ export function PhoneNumber() {
verificationCode={verificationCode} verificationCode={verificationCode}
setVerificationCode={setVerificationCode} setVerificationCode={setVerificationCode}
isVerified={isVerified} isVerified={isVerified}
isVerifying={isVerifying} isVerifying={isVerifying || isSendingCode || isChangingPhone}
buttonState={buttonState} buttonState={buttonState}
setButtonState={setButtonState} setButtonState={setButtonState}
handleSendCode={handleSendCode} handleSendCode={handleSendCode}
handleVerifyClick={handleVerifyCode} handleVerifyClick={handleVerifyCode}
error={error} error={combinedError}
inputError={inputError} inputError={inputError}
handleBlur={handleBlur} handleBlur={handleBlur}
textFieldRef={textFieldRef as React.RefObject<HTMLDivElement>} textFieldRef={textFieldRef as React.RefObject<HTMLDivElement>}

View File

@@ -2,7 +2,7 @@ import { Box, Typography, Avatar } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DisplayField } from './DisplayField'; import { DisplayField } from './DisplayField';
import { CountryFlag } from '@/components/CountryFlag'; import { CountryFlag } from '@/components/CountryFlag';
import { Gender } from '@/features/profile/types'; import { Gender } from '@/features/profile/types/settingsType';
interface InfoRowData { interface InfoRowData {
firstName: string; firstName: string;

View File

@@ -10,8 +10,8 @@ import {
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { countries } from '@/features/profile/data/countries'; import { countries } from '@/features/profile/data/countries';
import { CountryFlag } from '@/components/CountryFlag'; import { CountryFlag } from '@/components/CountryFlag';
import { Gender } from '@/features/profile/types'; import { Gender } from '@/features/profile/types/settingsType';
import { type InfoRowData } from '@/features/profile/types'; import { type InfoRowData } from '@/features/profile/types/settingsType';
interface InfoRowEditProps { interface InfoRowEditProps {
data: InfoRowData; data: InfoRowData;

View File

@@ -0,0 +1,35 @@
import { Gender } from './settingsType';
/* Profile API Types */
export interface GetProfileApiResponse {
success: boolean;
message?: string;
firstName?: string;
lastName?: string;
nationalCode?: string;
gender?: Gender;
countryCode?: string;
profileImageUrl?: string;
phoneNumber?: string; // ✅ ADDED
}
export interface SaveProfileApiResponse {
success: boolean;
message?: string;
phoneNumber?: string;
email?: string;
}
export interface TokenApiResponse {
access_token: string;
}
/* Phone Number API Types */
export interface PhoneNumberApiResponse {
success: boolean;
message?: string;
}
export interface ConfirmPhoneNumberApiResponse extends PhoneNumberApiResponse {
confirm?: boolean;
}

View File

@@ -1,6 +1,6 @@
export enum Gender { export enum Gender {
Male = 1, Male = 2,
Female = 2, Female = 1,
None = 0, None = 0,
} }

View File

@@ -1,26 +1,24 @@
import { useState, useEffect } from 'react'; import { useState } from 'react';
type ApiFunction<T> = () => Promise<{ data: T }>; type ApiFunction<T, P> = (params?: P) => Promise<{ data: T }>;
export function useApi<T>(apiFunction: ApiFunction<T>) { export function useManualApi<T, P>(apiFunction: ApiFunction<T, P>) {
const [data, setData] = useState<T | null>(null); const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(false);
const [error, setError] = useState<unknown>(null); const [error, setError] = useState<unknown>(null);
useEffect(() => { const execute = async (params?: P) => {
const fetchData = async () => { setLoading(true);
try { setError(null);
const response = await apiFunction(); try {
setData(response.data); const response = await apiFunction(params);
} catch (err) { setData(response.data);
setError(err); } catch (err) {
} finally { setError(err);
setLoading(false); } finally {
} setLoading(false);
}; }
};
fetchData(); return { data, loading, error, execute };
}, [apiFunction]);
return { data, loading, error };
} }