diff --git a/.env b/.env index cfdb88a..0eab989 100644 --- a/.env +++ b/.env @@ -3,4 +3,5 @@ VITE_DEFUALT_AUTH_RETURN_URL=/setting/profile VITE_API_URL=https://accounts.business-harmony.com/api/ VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token VITE_IDENTITY_CLIENT_ID=harmony_identity -VITE_IDENTITY_SCOPE=openid profile offline_access harmony_identity \ No newline at end of file +VITE_IDENTITY_SCOPE=openid profile offline_access harmony_identity +IMAGE_BASE_URL=https://accounts.business-harmony.com/uploads/ diff --git a/src/features/authorization/api/userCompletion.ts b/src/features/authorization/api/userCompletion.ts index d9fcf26..efb03fc 100644 --- a/src/features/authorization/api/userCompletion.ts +++ b/src/features/authorization/api/userCompletion.ts @@ -2,31 +2,19 @@ import { type SendEmailOtpPayload, type ConfirmEmailOtpPayload, type CompleteUserInfoPayload, - type CompleteUserInfoResponse, - type GenericApiResponse, } from '../types/completionFormApiTypes'; import apiClient from '@/lib/apiClient'; -export const sendEmailOtpApi = async ( - payload: SendEmailOtpPayload, -): Promise => { - const { data } = await apiClient.post('/User/SendEmailOtp', payload); - return data; +export const sendEmailOtpApi = async (payload: SendEmailOtpPayload) => { + return apiClient.post('/User/SendEmailOtp', payload); }; -export const confirmEmailOtpApi = async ( - payload: ConfirmEmailOtpPayload, -): Promise => { - const { data } = await apiClient.post('/User/ConfirmEmailOtp', payload); - return data; +export const confirmEmailOtpApi = async (payload: ConfirmEmailOtpPayload) => { + return apiClient.post('/User/ConfirmEmailOtp', payload); }; export const completeUserInformationApi = async ( payload: CompleteUserInfoPayload, -): Promise => { - const { data } = await apiClient.post( - '/User/CompleteUserInformation', - payload, - ); - return data; +) => { + return apiClient.post('/User/CompleteUserInformation', payload); }; diff --git a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx index 6e412f6..602ca73 100644 --- a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx @@ -55,11 +55,8 @@ export const EnterPasswordForm = ({ useApi(sendSmsOtp); const { loading: emailResendLoading, execute: emailResendCall } = useApi(sendEmailOtp); - const { - data: loginWithPassResult, - loading: loginWithPassLoading, - execute: loginWithPassCall, - } = useApi(loginWithPassword); + const { loading: loginWithPassLoading, execute: loginWithPassCall } = + useApi(loginWithPassword); const auth = useAuth(); const handleBlur = () => { diff --git a/src/features/authorization/routes/UserCompletionPage.tsx b/src/features/authorization/routes/UserCompletionPage.tsx index 8e86c90..6286474 100644 --- a/src/features/authorization/routes/UserCompletionPage.tsx +++ b/src/features/authorization/routes/UserCompletionPage.tsx @@ -16,12 +16,6 @@ import { confirmEmailOtpApi, completeUserInformationApi, } from '../api/userCompletion'; -import { - type SendEmailOtpPayload, - type ConfirmEmailOtpPayload, - type CompleteUserInfoPayload, -} from '../types/completionFormApiTypes'; -import { type ApiResponse } from '@/types/apiResponse'; import { containsSymbol } from '@/utils/regexes/containsSymbol'; import { least8Chars } from '@/utils/regexes/least8Chars'; import { containsNumber } from '@/utils/regexes/containsNumber'; @@ -63,46 +57,20 @@ export function UserCompletionPage() { const matchPassword = password === confirmPassword; const correctEmail = isEmail(email); - const { execute: sendCode, data: sendCodeData } = useApi( - async (payload: SendEmailOtpPayload) => { - const result = await sendEmailOtpApi(payload); - const conformingResult: ApiResponse = { - ...result, - errorCode: result.errorCode || 0, - validations: [], - }; - return { data: conformingResult }; - }, - ); + const { execute: sendCode, data: sendCodeData } = useApi(sendEmailOtpApi); 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 }; - }); + loading: isVerifyingCode, + } = useApi(confirmEmailOtpApi); const { execute: submitForm, + data: submitData, 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 }; - }); + } = useApi(completeUserInformationApi); const getErrorMessage = (error: unknown): string | null => { if (!error) return null; @@ -112,9 +80,10 @@ export function UserCompletionPage() { useEffect(() => { if (sendCodeData) { - if (sendCodeData.success) { + if (sendCodeData.data.success) { showToast({ - message: sendCodeData.message || t('completion.successfullCodeSent'), + message: + sendCodeData.data.message || t('completion.successfullCodeSent'), severity: 'success', }); setCodeSent(true); @@ -122,7 +91,7 @@ export function UserCompletionPage() { setCountdown(120); } else { showToast({ - message: sendCodeData.message || t('completion.problem'), + message: sendCodeData.data.message || t('completion.problem'), severity: 'error', }); } @@ -131,15 +100,15 @@ export function UserCompletionPage() { useEffect(() => { if (verifyCodeData) { - if (verifyCodeData.success) { + if (verifyCodeData.data.success) { setEmailVerified(true); showToast({ - message: verifyCodeData.message || t('completion.codeVerified'), + message: verifyCodeData.data.message || t('completion.codeVerified'), severity: 'success', }); } else { showToast({ - message: verifyCodeData.message || t('completion.invalidCode'), + message: verifyCodeData.data.message || t('completion.invalidCode'), severity: 'error', }); setEmailVerified(false); @@ -151,13 +120,13 @@ export function UserCompletionPage() { if (submitData) { showToast({ message: - submitData.message || + submitData.data.message || t( - submitData.success + submitData.data.success ? 'completion.submitSuccess' : 'completion.submitError', ), - severity: submitData.success ? 'success' : 'error', + severity: submitData.data.success ? 'success' : 'error', }); } else if (submitError) { showToast({ @@ -205,7 +174,6 @@ export function UserCompletionPage() { const handleSubmit = () => { submitForm({ - userId: '3fa85f64-5717-4562-b3fc-2c963f66afa6', firstName, lastName, gender: sex, @@ -325,7 +293,7 @@ export function UserCompletionPage() { onSubmit={handleSubmit} loading={isSubmitting} error={getErrorMessage(submitError)} - success={!!submitData?.success} + success={!!submitData?.data.success} /> diff --git a/src/features/authorization/types/completionFormApiTypes.ts b/src/features/authorization/types/completionFormApiTypes.ts index 303deca..5bd651d 100644 --- a/src/features/authorization/types/completionFormApiTypes.ts +++ b/src/features/authorization/types/completionFormApiTypes.ts @@ -14,7 +14,6 @@ export interface ConfirmEmailOtpPayload { } export interface CompleteUserInfoPayload { - userId: string; firstName: string; lastName: string; gender: 0 | 1 | 2; diff --git a/src/features/profile/api/settingsApi.ts b/src/features/profile/api/settingsApi.ts index bec087f..f840d7f 100644 --- a/src/features/profile/api/settingsApi.ts +++ b/src/features/profile/api/settingsApi.ts @@ -20,11 +20,29 @@ export async function fetchProfile() { export async function saveProfile(payload: { data: InfoRowData; - imageUrl: string | null; + imageUrl: File | null; }) { + const { data, imageUrl } = payload; + + 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)); + formData.append('CountryCode', data.country || ''); + + if (imageUrl) { + formData.append('Image', imageUrl); + } + return apiClient.post( '/Profile/SaveProfilePersonalInforamtion', - payload, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, ); } @@ -70,7 +88,7 @@ export async function deleteSessions(payload: { keys: string[] }) { ); } -export async function addPassword(payload: { password: string }) { +export async function addPassword(payload: { newPassword: string }) { return apiClient.post('/Profile/AddPassword', payload); } diff --git a/src/features/profile/components/security/PasswordDialog.tsx b/src/features/profile/components/security/PasswordDialog.tsx index 9077c94..a0bdf2d 100644 --- a/src/features/profile/components/security/PasswordDialog.tsx +++ b/src/features/profile/components/security/PasswordDialog.tsx @@ -14,6 +14,7 @@ import { import { CloseCircle } from 'iconsax-react'; import { Icon } from '@rkheftan/harmony-ui'; import { type PasswordDialogProps } from '../../types/settingsType'; +import { PasswordValidationItem } from './PasswordValidation'; export function PasswordDialog({ open, @@ -33,6 +34,10 @@ export function PasswordDialog({ changePassword, apiError, t, + hasMinLength, + hasNumber, + hasUpperAndLower, + hasSpecialChar, }: PasswordDialogProps) { return ( - {/* ✅ 4. API ERROR DISPLAY ADDED */} {apiError && ( {apiError} @@ -94,18 +98,28 @@ export function PasswordDialog({ fullWidth value={password} onChange={(e) => setPassword(e.target.value)} - sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} + sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }} /> {showValidation && ( - - {validPassword - ? t('securityForm.passwordIsValid') - : t('securityForm.passwordIsInvalid')} - + + + + + + )} { if (addData?.success || changeData?.success) { + showToast({ + message: changePasswordUI + ? t('securityForm.passwordChanged') + : t('securityForm.passwordAdded'), + severity: 'success', + }); if (!changePasswordUI) { setChangePasswordUI(true); } @@ -87,7 +95,17 @@ export function PasswordSecurity() { setConfirmPassword(''); setCurrentPassword(''); } - }, [addData, changeData, changePasswordUI]); + }, [addData, changeData, changePasswordUI, showToast, t]); + + useEffect(() => { + if (addError || changeError) { + showToast({ + message: + getErrorMessage(addError || changeError) || t('securityForm.general'), + severity: 'error', + }); + } + }, [addError, changeError, t, showToast]); const handleOpen = () => setOpen(true); const handleClose = () => setOpen(false); @@ -99,7 +117,7 @@ export function PasswordSecurity() { newPassword: password, }); } else { - await executeAddPassword({ password }); + await executeAddPassword({ newPassword: password }); } }; @@ -120,7 +138,12 @@ export function PasswordSecurity() { title={t('securityForm.password')} subtitle={t('securityForm.determinePassword')} action={ - - - - ) : ( + {isEditing ? ( + <> - )} - + + + ) : ( + + )} {getErrorMessage(saveProfileError) && ( { + setUploadedImageFile(file); const reader = new FileReader(); reader.onload = () => setUploadedImageUrl(reader.result as string); reader.readAsDataURL(file); }} - onRemoveImage={() => setUploadedImageUrl(null)} + onRemoveImage={() => { + setUploadedImageFile(null); + setUploadedImageUrl(null); + }} /> )} {data && diff --git a/src/features/profile/components/userInformation/PhoneNumber.tsx b/src/features/profile/components/userInformation/PhoneNumber.tsx index 5133179..266f64e 100644 --- a/src/features/profile/components/userInformation/PhoneNumber.tsx +++ b/src/features/profile/components/userInformation/PhoneNumber.tsx @@ -16,9 +16,11 @@ import { changePhoneNumber, } from '../../api/settingsApi'; import { type Phone } from '../../types/settingsType'; +import { useToast } from '@rkheftan/harmony-ui'; export function PhoneNumber() { const { t, i18n } = useTranslation('setting'); + const toast = useToast(); const [isEditing, setIsEditing] = useState(false); const [phoneNumber, setPhoneNumber] = useState(''); const [verificationCode, setVerificationCode] = useState(''); @@ -74,12 +76,36 @@ export function PhoneNumber() { } }, [profileData]); + useEffect(() => { + if (fetchError) { + toast({ + message: + getErrorMessage(fetchError) || t('settingForm.errorFetchPhoneNumber'), + severity: 'error', + }); + } + }, [fetchError, toast, t]); + useEffect(() => { if (sendCodeData?.success) { setButtonState('counting'); setIsVerified(false); + toast({ + message: t('settingForm.codeSentSuccessfully'), + severity: 'success', + }); } - }, [sendCodeData]); + }, [sendCodeData, t, toast]); + + useEffect(() => { + if (sendCodeError) { + toast({ + message: + getErrorMessage(sendCodeError) || t('settingForm.errorSendCode'), + severity: 'error', + }); + } + }, [sendCodeError, toast, t]); useEffect(() => { if (confirmData?.success && confirmData.confirm) { @@ -87,8 +113,22 @@ export function PhoneNumber() { setShowToast(true); const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, ''); executeChangePhone({ phoneNumber: fullPhoneNumber }); + toast({ + message: t('settingForm.phoneVerified'), + severity: 'success', + }); } - }, [confirmData, countryCode, phoneNumber, executeChangePhone]); + }, [confirmData, countryCode, phoneNumber, executeChangePhone, t, toast]); + + useEffect(() => { + if (confirmError) { + toast({ + message: + getErrorMessage(confirmError) || t('settingForm.errorConfirmCode'), + severity: 'error', + }); + } + }, [confirmError, toast, t]); useEffect(() => { if (changePhoneData?.success) { @@ -101,8 +141,23 @@ export function PhoneNumber() { }, ]); setIsEditing(false); + toast({ + message: t('settingForm.phoneChangedSuccessfully'), + severity: 'success', + }); } - }, [changePhoneData, countryCode, phoneNumber, t]); + }, [changePhoneData, countryCode, phoneNumber, t, toast]); + + useEffect(() => { + if (changePhoneError) { + toast({ + message: + getErrorMessage(changePhoneError) || + t('settingForm.errorChangePhone'), + severity: 'error', + }); + } + }, [changePhoneError, toast, t]); const apiError = useMemo( () => sendCodeError || confirmError || changePhoneError, @@ -151,7 +206,6 @@ export function PhoneNumber() { const handleSendCode = () => { handleBlur(); if (formError || !isPhoneValid(countryCode, phoneNumber)) return; - executeSendCode({ phoneNumber: countryCode + phoneNumber.replace(/^0/, ''), }); diff --git a/src/features/profile/components/userInformation/SocialMedia.tsx b/src/features/profile/components/userInformation/SocialMedia.tsx index 8c150f4..1c3ba2b 100644 --- a/src/features/profile/components/userInformation/SocialMedia.tsx +++ b/src/features/profile/components/userInformation/SocialMedia.tsx @@ -16,6 +16,7 @@ import { changeEmail, } from '../../api/settingsApi'; import { type EmailAccount } from '../../types/settingsType'; +import { useToast } from '@rkheftan/harmony-ui'; export function SocialMedia() { const { t } = useTranslation('setting'); @@ -28,6 +29,7 @@ export function SocialMedia() { ); const [emailList, setEmailList] = useState([]); const [formError, setFormError] = useState(null); + const showToast = useToast(); const { data: profileData, @@ -82,15 +84,23 @@ export function SocialMedia() { useEffect(() => { if (sendCodeData?.success) { + showToast({ + message: t('settingForm.verificationCodeSent'), + severity: 'success', + }); setDialogStep('enterCode'); } - }, [sendCodeData]); + }, [sendCodeData, t, showToast]); useEffect(() => { if (confirmData?.success && confirmData.confirm) { + showToast({ + message: t('settingForm.verificationSuccessful'), + severity: 'success', + }); executeChangeEmail({ email: emailInput }); } - }, [confirmData, emailInput, executeChangeEmail]); + }, [confirmData, emailInput, executeChangeEmail, t, showToast]); useEffect(() => { if (changeEmailData?.success) { @@ -102,9 +112,34 @@ export function SocialMedia() { time: t('settingForm.justNow'), }, ]); + showToast({ + message: t('settingForm.emailChangedSuccessfully'), + severity: 'success', + }); resetDialog(); } - }, [changeEmailData, emailInput, t]); + }, [changeEmailData, emailInput, t, showToast]); + + useEffect(() => { + if (sendCodeError) { + showToast({ + message: getErrorMessage(sendCodeError) || t('settingForm.error'), + severity: 'error', + }); + } + if (confirmError) { + showToast({ + message: getErrorMessage(confirmError) || t('settingForm.error'), + severity: 'error', + }); + } + if (changeEmailError) { + showToast({ + message: getErrorMessage(changeEmailError) || t('settingForm.error'), + severity: 'error', + }); + } + }, [sendCodeError, confirmError, changeEmailError, t, showToast]); const resetDialog = () => { setOpenDialog(false); @@ -115,7 +150,7 @@ export function SocialMedia() { }; const handleSendCode = () => { - if (!/^\S+@\S+\.\S+$/.test(emailInput)) { + if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(emailInput)) { setFormError(t('settingForm.emailIsInvalid')); return; } diff --git a/src/features/profile/components/userInformation/personalInformation/ProfileImage.tsx b/src/features/profile/components/userInformation/personalInformation/ProfileImage.tsx index 4baa6ad..be56d4b 100644 --- a/src/features/profile/components/userInformation/personalInformation/ProfileImage.tsx +++ b/src/features/profile/components/userInformation/personalInformation/ProfileImage.tsx @@ -68,6 +68,9 @@ export function ProfileImage({ sx={{ borderRadius: 2, textTransform: 'none', + flexShrink: 0, + flexGrow: 0, + width: 'auto', }} startIcon={ = 8 && digitsOnly.length <= 15; }; + const [isSending, setIsSending] = useState(false); + return ( <> @@ -73,7 +76,7 @@ export default function PhoneEditForm({ error={inputError} helperText={inputError ? error : ''} onChange={(e) => setPhoneNumber(e.target.value)} - sx={{ flex: '1 1 220px', minWidth: 0 }} + sx={{ flex: '1 1 220px' }} placeholder="09123456789" InputProps={{ endAdornment: @@ -122,12 +125,18 @@ export default function PhoneEditForm({ ) : (