chore: fix styles and change the apis
This commit is contained in:
1772
package-lock.json
generated
1772
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,36 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
type SendEmailOtpPayload,
|
type SendEmailOtpPayload,
|
||||||
type TokenApiResponse,
|
|
||||||
type ConfirmEmailOtpPayload,
|
type ConfirmEmailOtpPayload,
|
||||||
type CompleteUserInfoPayload,
|
type CompleteUserInfoPayload,
|
||||||
type CompleteUserInfoResponse,
|
type CompleteUserInfoResponse,
|
||||||
type GenericApiResponse,
|
type GenericApiResponse,
|
||||||
} from './types';
|
} from '../types/completionFormApiTypes';
|
||||||
import axios from 'axios';
|
|
||||||
import apiClient from '@/lib/apiClient';
|
import apiClient from '@/lib/apiClient';
|
||||||
|
|
||||||
const AUTH_API_URL = 'https://accounts.business-harmony.com';
|
|
||||||
|
|
||||||
export const getTokenApi = async (): Promise<TokenApiResponse> => {
|
|
||||||
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 { data } = await axios.post<TokenApiResponse>(
|
|
||||||
`${AUTH_API_URL}/connect/token`,
|
|
||||||
body.toString(),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sendEmailOtpApi = async (
|
export const sendEmailOtpApi = async (
|
||||||
payload: SendEmailOtpPayload,
|
payload: SendEmailOtpPayload,
|
||||||
): Promise<GenericApiResponse & { codeSentAnyway?: boolean }> => {
|
): Promise<GenericApiResponse & { codeSentAnyway?: boolean }> => {
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ import { format as formatJalali } from 'date-fns-jalali';
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||||
|
import { type DateOfBirthProps } from '../types/settingForm';
|
||||||
interface DateOfBirthProps {
|
|
||||||
value: Date | null;
|
|
||||||
onChange: (date: Date | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||||
const { t, i18n } = useTranslation('completionForm');
|
const { t, i18n } = useTranslation('completionForm');
|
||||||
|
|||||||
@@ -13,24 +13,7 @@ import {
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { TickCircle, Edit } from 'iconsax-react';
|
import { TickCircle, Edit } from 'iconsax-react';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
|
import { type EmailSectionProps } from '../types/settingForm';
|
||||||
interface EmailSectionProps {
|
|
||||||
showEmail: boolean;
|
|
||||||
setShowEmail: (checked: boolean) => void;
|
|
||||||
email: string;
|
|
||||||
setEmail: (email: string) => void;
|
|
||||||
correctEmail: boolean;
|
|
||||||
codeSent: boolean;
|
|
||||||
verificationCode: string;
|
|
||||||
setVerificationCode: (code: string) => void;
|
|
||||||
buttonState: 'default' | 'counting' | 'sent';
|
|
||||||
getButtonLabel: () => string;
|
|
||||||
handleSendCode: () => void;
|
|
||||||
handleVerifyCode: () => void;
|
|
||||||
emailVerified: boolean;
|
|
||||||
loading: boolean;
|
|
||||||
handleEditEmail: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EmailSection({
|
export function EmailSection({
|
||||||
showEmail,
|
showEmail,
|
||||||
|
|||||||
@@ -12,22 +12,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { TickCircle, Eye, EyeSlash, CloseCircle } from 'iconsax-react';
|
import { TickCircle, Eye, EyeSlash, CloseCircle } from 'iconsax-react';
|
||||||
import { PasswordValidationItem } from './PasswordValidation';
|
import { PasswordValidationItem } from './PasswordValidation';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
|
import { type PasswordSectionProps } from '../types/settingForm';
|
||||||
interface PasswordSectionProps {
|
|
||||||
showPasswordSection: boolean;
|
|
||||||
setShowPasswordSection: (checked: boolean) => void;
|
|
||||||
password: string;
|
|
||||||
setPassword: (password: string) => void;
|
|
||||||
confirmPassword: string;
|
|
||||||
setConfirmPassword: (confirmPassword: string) => void;
|
|
||||||
matchPassword: boolean;
|
|
||||||
hasNumber: boolean;
|
|
||||||
hasMinLength: boolean;
|
|
||||||
hasUpperAndLower: boolean;
|
|
||||||
hasSpecialChar: boolean;
|
|
||||||
validPassword: boolean;
|
|
||||||
showValidations: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PasswordSection({
|
export function PasswordSection({
|
||||||
showPasswordSection,
|
showPasswordSection,
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import { TickCircle } from 'iconsax-react';
|
import { TickCircle } from 'iconsax-react';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
|
import { type ValidationItemProps } from '../types/settingForm';
|
||||||
interface ValidationItemProps {
|
|
||||||
isValid: boolean;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PasswordValidationItem({
|
export function PasswordValidationItem({
|
||||||
isValid,
|
isValid,
|
||||||
|
|||||||
@@ -10,27 +10,12 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Woman, Man } from 'iconsax-react';
|
import { Woman, Man } from 'iconsax-react';
|
||||||
import { type Dispatch, type SetStateAction } from 'react';
|
|
||||||
import DateOfBirth from './DateOfBirth';
|
import DateOfBirth from './DateOfBirth';
|
||||||
import { countries } from '../data/Countries';
|
import { countries } from '../data/Countries';
|
||||||
import { CountryFlag } from '@/components/CountryFlag';
|
import { CountryFlag } from '@/components/CountryFlag';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
import { Gender } from './types';
|
import { type PersonalInfoFieldsProps } from '../types/settingForm';
|
||||||
|
import { Gender } from '../types/settingForm';
|
||||||
interface PersonalInfoFieldsProps {
|
|
||||||
firstName: string;
|
|
||||||
setFirstName: (v: string) => void;
|
|
||||||
lastName: string;
|
|
||||||
setLastName: (v: string) => void;
|
|
||||||
sex: Gender;
|
|
||||||
setSex: Dispatch<SetStateAction<Gender>>;
|
|
||||||
country: string;
|
|
||||||
setCountry: (country: string) => void;
|
|
||||||
nationalId: string;
|
|
||||||
setNationalId: (v: string) => void;
|
|
||||||
birthDate: Date | null;
|
|
||||||
setBirthDate: (d: Date | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PersonalInfoFields({
|
export function PersonalInfoFields({
|
||||||
firstName,
|
firstName,
|
||||||
|
|||||||
@@ -9,13 +9,9 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
interface Props {
|
import { type SubmitProps } from '../types/settingForm';
|
||||||
onSubmit: () => void;
|
|
||||||
loading: boolean;
|
export function SubmitSection({ onSubmit, loading, error }: SubmitProps) {
|
||||||
error: string | null;
|
|
||||||
success: boolean;
|
|
||||||
}
|
|
||||||
export function SubmitSection({ onSubmit, loading, error, success }: Props) {
|
|
||||||
const { t, i18n } = useTranslation('completionForm');
|
const { t, i18n } = useTranslation('completionForm');
|
||||||
const [openDialog, setOpenDialog] = useState(false);
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,53 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Box, Typography, Button } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Logo from '@/components/Logo';
|
import Logo from '@/components/Logo';
|
||||||
import { PersonalInfoFields } from './PersonalInfoFields';
|
import { PersonalInfoFields } from './PersonalInfoFields';
|
||||||
import { PasswordSection } from './PasswordSection';
|
import { PasswordSection } from './PasswordSection';
|
||||||
import { EmailSection } from './EmailSection';
|
import { EmailSection } from './EmailSection';
|
||||||
import { SubmitSection } from './SubmitSection';
|
import { SubmitSection } from './SubmitSection';
|
||||||
import apiClient from '@/lib/apiClient';
|
|
||||||
import { useToast } from '@rkheftan/harmony-ui';
|
import { useToast } from '@rkheftan/harmony-ui';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { regex } from '../../../utils/regex';
|
import { regex } from '../../../utils/regex';
|
||||||
import { toLocaleDigits } from '../../../utils/persianDigit';
|
import { toLocaleDigits } from '../../../utils/persianDigit';
|
||||||
import axios, { isAxiosError } from 'axios';
|
|
||||||
import i18n from '@/config/i18n';
|
import i18n from '@/config/i18n';
|
||||||
import { Gender } from './types'; // ✅ Added
|
import { Gender } from '../types/settingForm';
|
||||||
|
import { useApi } from '@/hooks/useApi';
|
||||||
interface TokenApiResponse {
|
import {
|
||||||
access_token: string;
|
sendEmailOtpApi,
|
||||||
}
|
confirmEmailOtpApi,
|
||||||
|
completeUserInformationApi,
|
||||||
|
} from '../api/userCompletion';
|
||||||
|
import {
|
||||||
|
type SendEmailOtpPayload,
|
||||||
|
type ConfirmEmailOtpPayload,
|
||||||
|
type CompleteUserInfoPayload,
|
||||||
|
} from '../types/completionFormApiTypes';
|
||||||
|
import { type ApiResponse } from '@/types/apiResponse';
|
||||||
|
|
||||||
export function UserCompletionForm() {
|
export function UserCompletionForm() {
|
||||||
const { t } = useTranslation('completionForm');
|
const { t } = useTranslation('completionForm');
|
||||||
|
const showToast = useToast();
|
||||||
|
|
||||||
const [firstName, setFirstName] = useState('');
|
const [firstName, setFirstName] = useState('');
|
||||||
const [lastName, setLastName] = useState('');
|
const [lastName, setLastName] = useState('');
|
||||||
const [nationalId, setNationalId] = useState('');
|
const [nationalId, setNationalId] = useState('');
|
||||||
const [birthDate, setBirthDate] = useState<Date | null>(null);
|
const [birthDate, setBirthDate] = useState<Date | null>(null);
|
||||||
|
|
||||||
// ✅ Corrected section: use Gender enum
|
|
||||||
const [sex, setSex] = useState<Gender>(Gender.Female);
|
const [sex, setSex] = useState<Gender>(Gender.Female);
|
||||||
const [country, setCountry] = useState('');
|
const [country, setCountry] = useState('');
|
||||||
|
|
||||||
const [showPasswordSection, setShowPasswordSection] = useState(false);
|
const [showPasswordSection, setShowPasswordSection] = useState(false);
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
|
||||||
const [showEmail, setShowEmail] = useState(false);
|
const [showEmail, setShowEmail] = useState(false);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [codeSent, setCodeSent] = useState(false);
|
|
||||||
const [verificationCode, setVerificationCode] = useState('');
|
const [verificationCode, setVerificationCode] = useState('');
|
||||||
|
|
||||||
|
const [codeSent, setCodeSent] = useState(false);
|
||||||
const [buttonState, setButtonState] = useState<'default' | 'counting'>(
|
const [buttonState, setButtonState] = useState<'default' | 'counting'>(
|
||||||
'default',
|
'default',
|
||||||
);
|
);
|
||||||
const [countdown, setCountdown] = useState(0);
|
const [countdown, setCountdown] = useState(0);
|
||||||
const [emailVerified, setEmailVerified] = useState(false);
|
const [emailVerified, setEmailVerified] = useState(false);
|
||||||
const [isVerifyingCode, setIsVerifyingCode] = useState(false);
|
const [showPasswordValidations, setShowPasswordValidations] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasNumber,
|
hasNumber,
|
||||||
@@ -55,25 +58,113 @@ export function UserCompletionForm() {
|
|||||||
correctEmail,
|
correctEmail,
|
||||||
} = regex(password, email);
|
} = regex(password, email);
|
||||||
const matchPassword = password === confirmPassword;
|
const matchPassword = password === confirmPassword;
|
||||||
const [showPasswordValidations, setShowPasswordValidations] = useState(false);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const { execute: sendCode, data: sendCodeData } = useApi(
|
||||||
const [error, setError] = useState<string | null>(null);
|
async (payload: SendEmailOtpPayload) => {
|
||||||
const [success, setSuccess] = useState(false);
|
const result = await sendEmailOtpApi(payload);
|
||||||
|
const conformingResult: ApiResponse = {
|
||||||
|
...result,
|
||||||
|
errorCode: result.errorCode || 0,
|
||||||
|
validations: [],
|
||||||
|
};
|
||||||
|
return { data: conformingResult };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const showToast = useToast();
|
const {
|
||||||
|
execute: verifyCode,
|
||||||
|
loading: isVerifyingCode,
|
||||||
|
data: verifyCodeData,
|
||||||
|
} = useApi(async (payload: ConfirmEmailOtpPayload) => {
|
||||||
|
const result = await confirmEmailOtpApi(payload);
|
||||||
|
const conformingResult: ApiResponse = {
|
||||||
|
...result,
|
||||||
|
errorCode: result.errorCode || 0,
|
||||||
|
validations: [],
|
||||||
|
};
|
||||||
|
return { data: conformingResult };
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
execute: submitForm,
|
||||||
|
loading: isSubmitting,
|
||||||
|
error: submitError,
|
||||||
|
data: submitData,
|
||||||
|
} = useApi(async (payload: CompleteUserInfoPayload) => {
|
||||||
|
const result = await completeUserInformationApi(payload);
|
||||||
|
const conformingResult: ApiResponse = {
|
||||||
|
...result,
|
||||||
|
errorCode: result.errorCode || 0,
|
||||||
|
validations: [],
|
||||||
|
};
|
||||||
|
return { data: conformingResult };
|
||||||
|
});
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown): string | null => {
|
||||||
|
if (!error) return null;
|
||||||
|
if (error instanceof Error) return error.message;
|
||||||
|
return String(error);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (password) {
|
if (sendCodeData) {
|
||||||
if (!validPassword) {
|
if (sendCodeData.success) {
|
||||||
setShowPasswordValidations(true);
|
showToast({
|
||||||
|
message: sendCodeData.message || t('completion.successfullCodeSent'),
|
||||||
|
severity: 'success',
|
||||||
|
});
|
||||||
|
setCodeSent(true);
|
||||||
|
setButtonState('counting');
|
||||||
|
setCountdown(120);
|
||||||
} else {
|
} else {
|
||||||
const timer = setTimeout(() => setShowPasswordValidations(false), 1000);
|
showToast({
|
||||||
return () => clearTimeout(timer);
|
message: sendCodeData.message || t('completion.problem'),
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}, [sendCodeData, showToast, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (verifyCodeData) {
|
||||||
|
if (verifyCodeData.success) {
|
||||||
|
setEmailVerified(true);
|
||||||
|
showToast({
|
||||||
|
message: verifyCodeData.message || t('completion.codeVerified'),
|
||||||
|
severity: 'success',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setShowPasswordValidations(false);
|
showToast({
|
||||||
|
message: verifyCodeData.message || t('completion.invalidCode'),
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
|
setEmailVerified(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}, [verifyCodeData, showToast, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitData) {
|
||||||
|
showToast({
|
||||||
|
message:
|
||||||
|
submitData.message ||
|
||||||
|
t(
|
||||||
|
submitData.success
|
||||||
|
? 'completion.submitSuccess'
|
||||||
|
: 'completion.submitError',
|
||||||
|
),
|
||||||
|
severity: submitData.success ? 'success' : 'error',
|
||||||
|
});
|
||||||
|
} else if (submitError) {
|
||||||
|
showToast({
|
||||||
|
message: getErrorMessage(submitError) || t('completion.problem'),
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [submitData, submitError, showToast, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowPasswordValidations(password ? !validPassword : false);
|
||||||
}, [password, validPassword]);
|
}, [password, validPassword]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -93,6 +184,37 @@ export function UserCompletionForm() {
|
|||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [buttonState, countdown]);
|
}, [buttonState, countdown]);
|
||||||
|
|
||||||
|
const handleSendCode = () => {
|
||||||
|
sendCode({ email });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyCode = () => {
|
||||||
|
if (!verificationCode.trim()) {
|
||||||
|
showToast({
|
||||||
|
message: 'Please enter the verification code',
|
||||||
|
severity: 'warning',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
verifyCode({ email, otpCode: verificationCode });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
submitForm({
|
||||||
|
userId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
gender: sex,
|
||||||
|
nationalId,
|
||||||
|
savePassword: showPasswordSection,
|
||||||
|
password: showPasswordSection ? password : undefined,
|
||||||
|
saveEmail: showEmail,
|
||||||
|
email: showEmail ? email : undefined,
|
||||||
|
birthDate,
|
||||||
|
country,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getButtonLabel = () => {
|
const getButtonLabel = () => {
|
||||||
if (buttonState === 'counting') {
|
if (buttonState === 'counting') {
|
||||||
const m = String(Math.floor(countdown / 60)).padStart(2, '0');
|
const m = String(Math.floor(countdown / 60)).padStart(2, '0');
|
||||||
@@ -102,142 +224,6 @@ export function UserCompletionForm() {
|
|||||||
return t('completion.vericationCodeButton');
|
return t('completion.vericationCodeButton');
|
||||||
};
|
};
|
||||||
|
|
||||||
const [tokenError, setTokenError] = useState<string | null>(null);
|
|
||||||
const apiUrl = 'https://accounts.business-harmony.com';
|
|
||||||
const tokenEndpoint = `${apiUrl}/connect/token`;
|
|
||||||
const getToken = async () => {
|
|
||||||
setTokenError(null);
|
|
||||||
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);
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const storedToken = localStorage.getItem('authToken');
|
|
||||||
|
|
||||||
const handleSendCode = async () => {
|
|
||||||
setError(null);
|
|
||||||
setLoading(true);
|
|
||||||
setSuccess(false);
|
|
||||||
try {
|
|
||||||
const response = await apiClient.post(
|
|
||||||
'/User/SendEmailOtp',
|
|
||||||
{ email },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${storedToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (response.data?.success) {
|
|
||||||
showToast({
|
|
||||||
message: response.data.message || t('completion.successfullCodeSent'),
|
|
||||||
severity: 'success',
|
|
||||||
});
|
|
||||||
setCodeSent(true);
|
|
||||||
setButtonState('counting');
|
|
||||||
setCountdown(120);
|
|
||||||
} else if (response.data?.codeSentAnyway) {
|
|
||||||
showToast({
|
|
||||||
message: t('completion.codeSentBut') + (response.data.message || ''),
|
|
||||||
severity: 'warning',
|
|
||||||
});
|
|
||||||
setCodeSent(true);
|
|
||||||
setButtonState('counting');
|
|
||||||
setCountdown(120);
|
|
||||||
} else {
|
|
||||||
showToast({
|
|
||||||
message: response.data.message || t('completion.problem'),
|
|
||||||
severity: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const err = error as AxiosError<{ message?: string }>;
|
|
||||||
|
|
||||||
showToast({
|
|
||||||
message: err.response?.data?.message || t('completion.problem'),
|
|
||||||
severity: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVerifyCode = async () => {
|
|
||||||
if (!verificationCode.trim()) {
|
|
||||||
setError('Please enter the verification code');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsVerifyingCode(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await apiClient.post(
|
|
||||||
'/User/ConfirmEmailOtp',
|
|
||||||
{
|
|
||||||
email,
|
|
||||||
otpCode: verificationCode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${storedToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.data?.success) {
|
|
||||||
setEmailVerified(true);
|
|
||||||
setSuccess(true);
|
|
||||||
showToast({
|
|
||||||
message: res.data.message || t('completion.codeVerified'),
|
|
||||||
severity: 'success',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
showToast({
|
|
||||||
message: res.data?.message || t('completion.invalidCode'),
|
|
||||||
severity: 'error',
|
|
||||||
});
|
|
||||||
setEmailVerified(false);
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const err = error as AxiosError<{ message?: string }>;
|
|
||||||
showToast({
|
|
||||||
message: err.response?.data?.message || '',
|
|
||||||
severity: 'error',
|
|
||||||
});
|
|
||||||
setEmailVerified(false);
|
|
||||||
} finally {
|
|
||||||
setIsVerifyingCode(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditEmail = () => {
|
const handleEditEmail = () => {
|
||||||
setButtonState('default');
|
setButtonState('default');
|
||||||
setCodeSent(false);
|
setCodeSent(false);
|
||||||
@@ -245,60 +231,6 @@ export function UserCompletionForm() {
|
|||||||
setVerificationCode('');
|
setVerificationCode('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
setSuccess(false);
|
|
||||||
try {
|
|
||||||
const { data } = await apiClient.post<{
|
|
||||||
success: boolean;
|
|
||||||
errorCode: number;
|
|
||||||
message: string;
|
|
||||||
validations: { property: string; message: string }[];
|
|
||||||
}>(
|
|
||||||
'/User/CompleteUserInformation',
|
|
||||||
{
|
|
||||||
userId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
gender: sex === Gender.Female ? 2 : 1, // ✅ Corrected
|
|
||||||
nationalId,
|
|
||||||
savePassword: showPasswordSection,
|
|
||||||
password: showPasswordSection ? password : undefined,
|
|
||||||
saveEmail: showEmail,
|
|
||||||
email: showEmail ? email : undefined,
|
|
||||||
birthDate,
|
|
||||||
country,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${storedToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (data.success) {
|
|
||||||
showToast({
|
|
||||||
message: data.message || t('completion.submitSuccess'),
|
|
||||||
severity: 'success',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
showToast({
|
|
||||||
message: data.message || t('completion.submitError'),
|
|
||||||
severity: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const err = error as AxiosError<{ message?: string }>;
|
|
||||||
showToast({
|
|
||||||
message:
|
|
||||||
err.response?.data?.message || err.message || t('completion.problem'),
|
|
||||||
severity: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -345,8 +277,8 @@ export function UserCompletionForm() {
|
|||||||
setNationalId={setNationalId}
|
setNationalId={setNationalId}
|
||||||
birthDate={birthDate}
|
birthDate={birthDate}
|
||||||
setBirthDate={setBirthDate}
|
setBirthDate={setBirthDate}
|
||||||
sex={sex} // ✅ Corrected
|
sex={sex}
|
||||||
setSex={setSex} // ✅ Corrected
|
setSex={setSex}
|
||||||
country={country}
|
country={country}
|
||||||
setCountry={setCountry}
|
setCountry={setCountry}
|
||||||
/>
|
/>
|
||||||
@@ -384,21 +316,13 @@ export function UserCompletionForm() {
|
|||||||
loading={isVerifyingCode}
|
loading={isVerifyingCode}
|
||||||
handleEditEmail={handleEditEmail}
|
handleEditEmail={handleEditEmail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SubmitSection
|
<SubmitSection
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
loading={loading}
|
loading={isSubmitting}
|
||||||
error={error}
|
error={getErrorMessage(submitError)}
|
||||||
success={success}
|
success={!!submitData?.success}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
onClick={getToken}
|
|
||||||
size="large"
|
|
||||||
sx={{ textTransform: 'none' }}
|
|
||||||
>
|
|
||||||
Get Token
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export enum Gender {
|
|
||||||
None = 0,
|
|
||||||
Female = 1,
|
|
||||||
Male = 2,
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,3 @@
|
|||||||
export interface TokenRequestPayload {
|
|
||||||
grant_type: 'password';
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
client_id: string;
|
|
||||||
scope: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TokenApiResponse {
|
|
||||||
access_token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GenericApiResponse {
|
export interface GenericApiResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
73
src/features/authentication/types/settingForm.ts
Normal file
73
src/features/authentication/types/settingForm.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { type Dispatch, type SetStateAction } from 'react';
|
||||||
|
|
||||||
|
export enum Gender {
|
||||||
|
None = 0,
|
||||||
|
Female = 1,
|
||||||
|
Male = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DateOfBirthProps {
|
||||||
|
value: Date | null;
|
||||||
|
onChange: (date: Date | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailSectionProps {
|
||||||
|
showEmail: boolean;
|
||||||
|
setShowEmail: (checked: boolean) => void;
|
||||||
|
email: string;
|
||||||
|
setEmail: (email: string) => void;
|
||||||
|
correctEmail: boolean;
|
||||||
|
codeSent: boolean;
|
||||||
|
verificationCode: string;
|
||||||
|
setVerificationCode: (code: string) => void;
|
||||||
|
buttonState: 'default' | 'counting' | 'sent';
|
||||||
|
getButtonLabel: () => string;
|
||||||
|
handleSendCode: () => void;
|
||||||
|
handleVerifyCode: () => void;
|
||||||
|
emailVerified: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
handleEditEmail: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordSectionProps {
|
||||||
|
showPasswordSection: boolean;
|
||||||
|
setShowPasswordSection: (checked: boolean) => void;
|
||||||
|
password: string;
|
||||||
|
setPassword: (password: string) => void;
|
||||||
|
confirmPassword: string;
|
||||||
|
setConfirmPassword: (confirmPassword: string) => void;
|
||||||
|
matchPassword: boolean;
|
||||||
|
hasNumber: boolean;
|
||||||
|
hasMinLength: boolean;
|
||||||
|
hasUpperAndLower: boolean;
|
||||||
|
hasSpecialChar: boolean;
|
||||||
|
validPassword: boolean;
|
||||||
|
showValidations: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationItemProps {
|
||||||
|
isValid: boolean;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonalInfoFieldsProps {
|
||||||
|
firstName: string;
|
||||||
|
setFirstName: (v: string) => void;
|
||||||
|
lastName: string;
|
||||||
|
setLastName: (v: string) => void;
|
||||||
|
sex: Gender;
|
||||||
|
setSex: Dispatch<SetStateAction<Gender>>;
|
||||||
|
country: string;
|
||||||
|
setCountry: (country: string) => void;
|
||||||
|
nationalId: string;
|
||||||
|
setNationalId: (v: string) => void;
|
||||||
|
birthDate: Date | null;
|
||||||
|
setBirthDate: (d: Date | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmitProps {
|
||||||
|
onSubmit: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
@@ -1,26 +1,46 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { type ApiResponse } from '@/types/apiResponse';
|
||||||
|
|
||||||
type ApiFunction<T> = () => Promise<{ data: T }>;
|
// Define options for the hook
|
||||||
|
interface UseApiOptions {
|
||||||
|
// If true, the API call will be executed immediately on mount
|
||||||
|
immediate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function useApi<T>(apiFunction: ApiFunction<T>) {
|
export function useApi<T extends ApiResponse, P extends any[]>(
|
||||||
|
apiFunction: (...args: P) => Promise<{ data: T }>,
|
||||||
|
options: UseApiOptions = {},
|
||||||
|
) {
|
||||||
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 = useCallback(
|
||||||
const fetchData = async () => {
|
async (...args: P) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiFunction();
|
const response = await apiFunction(...args);
|
||||||
|
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// TODO: can handle some common errors here, 400 and 500 errors
|
||||||
setError(err);
|
setError(err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[apiFunction],
|
||||||
|
);
|
||||||
|
|
||||||
fetchData();
|
// If the 'immediate' option is true, execute the function on mount
|
||||||
}, [apiFunction]);
|
useEffect(() => {
|
||||||
|
if (options.immediate) {
|
||||||
return { data, loading, error };
|
// We pass undefined as params for the initial call.
|
||||||
|
execute(...(undefined as unknown as P));
|
||||||
|
}
|
||||||
|
}, [execute, options.immediate]);
|
||||||
|
|
||||||
|
return { data, loading, error, execute };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ const apiClient = axios.create({
|
|||||||
|
|
||||||
// Set default headers
|
// Set default headers
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('authToken')}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
13
src/types/apiResponse.ts
Normal file
13
src/types/apiResponse.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export interface ApiResponse {
|
||||||
|
success: boolean;
|
||||||
|
errorCode: number;
|
||||||
|
message: string;
|
||||||
|
validations: ApiResponseValidation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponseValidation {
|
||||||
|
message: string;
|
||||||
|
code: number;
|
||||||
|
property: string;
|
||||||
|
severity: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user