chore: move api to another component
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
122
src/features/profile/api/settingsApi.ts
Normal file
122
src/features/profile/api/settingsApi.ts
Normal 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 };
|
||||||
|
}
|
||||||
@@ -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 {
|
useEffect(() => {
|
||||||
const res = await apiClient.post<GetProfileApiResponse>(
|
if (profileData?.success) {
|
||||||
'/Profile/GetProfile',
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${storedToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (res.data?.success) {
|
|
||||||
const profile = res.data;
|
|
||||||
const fetchedData = {
|
const fetchedData = {
|
||||||
firstName: profile.firstName ?? '',
|
firstName: profileData.firstName ?? '',
|
||||||
lastName: profile.lastName ?? '',
|
lastName: profileData.lastName ?? '',
|
||||||
nationalCode: profile.nationalCode ?? '',
|
nationalCode: profileData.nationalCode ?? '',
|
||||||
gender: Object.values(Gender).includes(profile.gender as Gender)
|
gender: Object.values(Gender).includes(profileData.gender as Gender)
|
||||||
? (profile.gender as Gender)
|
? (profileData.gender as Gender)
|
||||||
: Gender.None,
|
: Gender.None,
|
||||||
country: profile.countryCode ?? '',
|
country: profileData.countryCode ?? '',
|
||||||
};
|
};
|
||||||
setData(fetchedData);
|
setData(fetchedData);
|
||||||
setOriginalData(fetchedData);
|
setOriginalData(fetchedData);
|
||||||
setUploadedImageUrl(profile.profileImageUrl || null);
|
setUploadedImageUrl(profileData.profileImageUrl || null);
|
||||||
} else {
|
|
||||||
throw new Error(res.data.message || t('settingForm.failRetrieve'));
|
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
}, [profileData]);
|
||||||
let message = t('settingForm.errorFetch');
|
|
||||||
if (error instanceof Error) {
|
|
||||||
message = error.message;
|
|
||||||
}
|
|
||||||
setFetchError(message);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (storedToken) {
|
useEffect(() => {
|
||||||
fetchProfile();
|
if (saveData?.success) {
|
||||||
} else {
|
setIsEditing(false);
|
||||||
setIsLoading(false);
|
setOriginalData(data);
|
||||||
setFetchError(t('settingForm.notLoggedIn'));
|
|
||||||
}
|
}
|
||||||
}, [storedToken, t]);
|
}, [saveData, data]);
|
||||||
|
|
||||||
const initials = `${data?.firstName?.trim()[0] || ''}${
|
const initials = `${data?.firstName?.trim()[0] || ''}${data?.lastName?.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={{
|
||||||
|
|||||||
@@ -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,171 +31,81 @@ 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) {
|
|
||||||
setIsLoading(false);
|
|
||||||
setFetchError(t('settingForm.notLoggedIn'));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
}, [isEditing]);
|
||||||
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,
|
phone: profileData.phoneNumber,
|
||||||
time: '',
|
time: '',
|
||||||
withCode: res.data.phoneNumber,
|
withCode: profileData.phoneNumber,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else if (!res.data.success) {
|
|
||||||
throw new Error(
|
|
||||||
res.data.message || t('settingForm.failFetchPhoneNumber'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
}, [profileData]);
|
||||||
let message = t('settingForm.errorFetchPhoneNumber');
|
|
||||||
if (err instanceof Error) {
|
|
||||||
message = err.message;
|
|
||||||
}
|
|
||||||
setFetchError(message);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchPhoneNumber();
|
useEffect(() => {
|
||||||
}, [token, t]);
|
if (sendCodeData?.success) {
|
||||||
|
|
||||||
const isPhoneValid = (code: string, phone: string) => {
|
|
||||||
const phoneNum = parsePhoneNumberFromString(code + phone);
|
|
||||||
return phoneNum && phoneNum.isValid();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlur = () => {
|
|
||||||
setTouched(true);
|
|
||||||
if (!phoneNumber) {
|
|
||||||
setError(t('settingForm.thisFieldIsRequired'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isPhoneValid(countryCode, phoneNumber)) {
|
|
||||||
setError(t('settingForm.phoneNumberIsInvalid'));
|
|
||||||
} else {
|
|
||||||
setError(undefined);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleEdit = () => {
|
|
||||||
setIsEditing((prev) => {
|
|
||||||
const enteringEditMode = !prev;
|
|
||||||
if (enteringEditMode) {
|
|
||||||
setPhoneNumber('');
|
|
||||||
setVerificationCode('');
|
|
||||||
setIsVerified(false);
|
|
||||||
setButtonState('default');
|
|
||||||
setShowToast(false);
|
|
||||||
setError(undefined);
|
|
||||||
setTouched(false);
|
|
||||||
}
|
|
||||||
return enteringEditMode;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendCode = async () => {
|
|
||||||
if (!phoneNumber) return;
|
|
||||||
if (!isPhoneValid(countryCode, phoneNumber)) {
|
|
||||||
setError(t('settingForm.phoneNumberIsInvalid'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setError(undefined);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await apiClient.post<ApiResponse>(
|
|
||||||
'/Profile/SendVerfiyPhoneNumberCode',
|
|
||||||
{
|
|
||||||
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
|
|
||||||
},
|
|
||||||
{ headers: { Authorization: `Bearer ${token}` } },
|
|
||||||
);
|
|
||||||
if (res.data.success) {
|
|
||||||
setButtonState('counting');
|
setButtonState('counting');
|
||||||
setIsVerified(false);
|
setIsVerified(false);
|
||||||
} else {
|
|
||||||
setError(res.data.message || t('settingForm.sendCodeFailed'));
|
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
}, [sendCodeData]);
|
||||||
setError(t('settingForm.sendCodeFailed'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVerifyCode = async () => {
|
useEffect(() => {
|
||||||
if (!verificationCode) {
|
if (confirmData?.success && confirmData.confirm) {
|
||||||
setError(t('settingForm.verificationCodeRequired'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsVerifying(true);
|
|
||||||
setError(undefined);
|
|
||||||
|
|
||||||
try {
|
|
||||||
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);
|
setIsVerified(true);
|
||||||
setShowToast(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 () => {
|
|
||||||
try {
|
|
||||||
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
|
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
|
||||||
const res = await apiClient.post<ApiResponse>(
|
executeChangePhone({ phoneNumber: fullPhoneNumber });
|
||||||
'/Profile/ChangePhoneNumber',
|
}
|
||||||
{
|
}, [confirmData, countryCode, phoneNumber, executeChangePhone]);
|
||||||
phoneNumber: fullPhoneNumber,
|
|
||||||
},
|
|
||||||
{ headers: { Authorization: `Bearer ${token}` } },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.data.success) {
|
useEffect(() => {
|
||||||
setPhone([
|
if (changePhoneData?.success) {
|
||||||
|
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
|
||||||
|
setPhones([
|
||||||
{
|
{
|
||||||
phone: phoneNumber,
|
phone: phoneNumber,
|
||||||
time: t('settingForm.justNow'),
|
time: t('settingForm.justNow'),
|
||||||
@@ -212,14 +113,77 @@ export function PhoneNumber() {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
} else {
|
|
||||||
setError(res.data.message || t('settingForm.changePhoneFailed'));
|
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
}, [changePhoneData, countryCode, phoneNumber, t]);
|
||||||
setError(t('settingForm.changePhoneFailed'));
|
|
||||||
|
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 (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<CardContainer
|
<CardContainer
|
||||||
@@ -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>}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
35
src/features/profile/types/settingsApiType.ts
Normal file
35
src/features/profile/types/settingsApiType.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export enum Gender {
|
export enum Gender {
|
||||||
Male = 1,
|
Male = 2,
|
||||||
Female = 2,
|
Female = 1,
|
||||||
None = 0,
|
None = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
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);
|
||||||
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await apiFunction();
|
const response = await apiFunction(params);
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err);
|
setError(err);
|
||||||
@@ -19,8 +20,5 @@ export function useApi<T>(apiFunction: ApiFunction<T>) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
return { data, loading, error, execute };
|
||||||
}, [apiFunction]);
|
|
||||||
|
|
||||||
return { data, loading, error };
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user