fix: accounts review
This commit is contained in:
2
.env
2
.env
@@ -5,4 +5,4 @@ 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
|
||||
IMAGE_BASE_URL=https://accounts.business-harmony.com/uploads/
|
||||
VITE_IMAGE_BASE_URL=https://accounts.business-harmony.com/api/uploads
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
"submitError": "Error in registering information",
|
||||
"submitting": "Submitting...",
|
||||
"success": "Success",
|
||||
"agreement": "1. Confidentiality of Information: Harmony commits under no circumstances to disclose users’ identity information, such as phone numbers, email addresses, passwords, user IDs, or any related data, to third parties.\n\n2. User information is used solely for providing authentication services and remains confidential even after account deactivation or termination.\n\n3. Harmony is obliged to implement necessary security measures to prevent unauthorized access.\n\n4. Responsibility for Account Security: Users must protect their accounts and choose strong, non-guessable passwords. Periodic password changes and immediate action in case of suspected unauthorized access are required. Any misuse of the account due to user negligence is the responsibility of the user.\n\n5. Security Breaches and Cyber Attacks: Harmony is not responsible for security breaches caused by cyber attacks beyond the system's control. However, Harmony employs up-to-date security standards and encryption to prevent such incidents.\n\n6. User Negligence in Protecting Information: If account information is disclosed due to user negligence or error, Harmony bears no responsibility. Determining such cases, based on system security logs, is the responsibility of Harmony's technical manager.\n\n7. Accurate Logging of Activities: All events related to registering, editing, and deleting information in the system are accurately and immutably logged. Claims regarding deletion or modification of data without logs are invalid unless supported by documentation provided by the user.\n\n8. Service Updates: Harmony services may be updated or changed over time. Continued use of the system after changes implies acceptance of the new terms. If users disagree, they may request account deletion.\n\n9. User Support: Support is provided only via email and phone, free of charge. Harmony is not obligated to provide in-person support or training beyond basic services.\n\n10. Official Communication Channels: Harmony communicates with users only via the phone number and email registered in the user account. Official announcements are sent through these channels.\n\n11. Official Domains for Communication: All emails from Harmony are sent exclusively from the domain harmony.id. Users must verify this to prevent phishing or similar attacks.\n\n12. Compliance with Iranian Laws: Users must comply with all applicable laws of the Islamic Republic of Iran, including the “Electronic Commerce Law,” “Computer Crimes Law,” and related legislation. Responsibility for violations rests with the user.\n\n13. Temporary Data Retention After Account Termination: Upon account termination or deletion, user information is stored securely for 30 days and permanently deleted thereafter.\n\n14. Ownership of User Data: All data submitted by users belongs to them. Harmony has no ownership over this information. Users are responsible for the accuracy, quality, and legality of their data.\n\n15. Purposeful Use of Identity Information: Collected identity information during registration is used only for authentication and basic services. It will not be shared with any third party without explicit user consent, except under a court order or legal authority.\n\n16. Permanent Data Confidentiality: Harmony commits to maintaining confidentiality of collected information even after the end of the user relationship or account closure.\n\n17. Limitation of Liability: Harmony is not liable for direct or indirect damages resulting from use or inability to use the authentication services.\n\n18. Disruptions in Communication Infrastructure: Harmony is not responsible for disruptions caused by the internet, infrastructure services, or other issues beyond its control.\n\n19. Force Majeure and Unforeseen Events: Harmony bears no responsibility for natural disasters, strikes, power outages, cyber attacks, or other events beyond its control that prevent service delivery.\n\n20. Services Dependent on Third Parties: If parts of the authentication services are provided by third parties, the usage terms of those services are the responsibility of those companies, and Harmony bears no liability.\n\n21. Guarantee of Data Access in Case of Service Termination: If Harmony ceases operations permanently, it commits to keeping servers active for two years and allowing users access to their data.\n\n22. Notification of Service Interruptions: If service interruption is necessary, Harmony must notify users at least 12 hours in advance via email or SMS."
|
||||
"agreement": "1. Confidentiality of Information: Harmony commits under no circumstances to disclose users’ identity information, such as phone numbers, email addresses, passwords, user IDs, or any related data, to third parties.\n\n2. User information is used solely for providing authentication services and remains confidential even after account deactivation or termination.\n\n3. Harmony is obliged to implement necessary security measures to prevent unauthorized access.\n\n4. Responsibility for Account Security: Users must protect their accounts and choose strong, non-guessable passwords. Periodic password changes and immediate action in case of suspected unauthorized access are required. Any misuse of the account due to user negligence is the responsibility of the user.\n\n5. Security Breaches and Cyber Attacks: Harmony is not responsible for security breaches caused by cyber attacks beyond the system's control. However, Harmony employs up-to-date security standards and encryption to prevent such incidents.\n\n6. User Negligence in Protecting Information: If account information is disclosed due to user negligence or error, Harmony bears no responsibility. Determining such cases, based on system security logs, is the responsibility of Harmony's technical manager.\n\n7. Accurate Logging of Activities: All events related to registering, editing, and deleting information in the system are accurately and immutably logged. Claims regarding deletion or modification of data without logs are invalid unless supported by documentation provided by the user.\n\n8. Service Updates: Harmony services may be updated or changed over time. Continued use of the system after changes implies acceptance of the new terms. If users disagree, they may request account deletion.\n\n9. User Support: Support is provided only via email and phone, free of charge. Harmony is not obligated to provide in-person support or training beyond basic services.\n\n10. Official Communication Channels: Harmony communicates with users only via the phone number and email registered in the user account. Official announcements are sent through these channels.\n\n11. Official Domains for Communication: All emails from Harmony are sent exclusively from the domain harmony.id. Users must verify this to prevent phishing or similar attacks.\n\n12. Compliance with Iranian Laws: Users must comply with all applicable laws of the Islamic Republic of Iran, including the “Electronic Commerce Law,” “Computer Crimes Law,” and related legislation. Responsibility for violations rests with the user.\n\n13. Temporary Data Retention After Account Termination: Upon account termination or deletion, user information is stored securely for 30 days and permanently deleted thereafter.\n\n14. Ownership of User Data: All data submitted by users belongs to them. Harmony has no ownership over this information. Users are responsible for the accuracy, quality, and legality of their data.\n\n15. Purposeful Use of Identity Information: Collected identity information during registration is used only for authentication and basic services. It will not be shared with any third party without explicit user consent, except under a court order or legal authority.\n\n16. Permanent Data Confidentiality: Harmony commits to maintaining confidentiality of collected information even after the end of the user relationship or account closure.\n\n17. Limitation of Liability: Harmony is not liable for direct or indirect damages resulting from use or inability to use the authentication services.\n\n18. Disruptions in Communication Infrastructure: Harmony is not responsible for disruptions caused by the internet, infrastructure services, or other issues beyond its control.\n\n19. Force Majeure and Unforeseen Events: Harmony bears no responsibility for natural disasters, strikes, power outages, cyber attacks, or other events beyond its control that prevent service delivery.\n\n20. Services Dependent on Third Parties: If parts of the authentication services are provided by third parties, the usage terms of those services are the responsibility of those companies, and Harmony bears no liability.\n\n21. Guarantee of Data Access in Case of Service Termination: If Harmony ceases operations permanently, it commits to keeping servers active for two years and allowing users access to their data.\n\n22. Notification of Service Interruptions: If service interruption is necessary, Harmony must notify users at least 12 hours in advance via email or SMS.",
|
||||
"successfulCodeSent": "Verification code sent successfully.",
|
||||
"problem": "There is a problem"
|
||||
},
|
||||
"validation": {
|
||||
"firstNameRequired": "First name is required.",
|
||||
|
||||
@@ -69,7 +69,8 @@
|
||||
"errorSendCode": "Failed to send code",
|
||||
"phoneVerified": "Phone number verified",
|
||||
"errorConfirmCode": "Failed to confirm code",
|
||||
"errorChangePhone": "Failed to change phone number"
|
||||
"errorChangePhone": "Failed to change phone number",
|
||||
"verificationCodeSent": "Verification code sent"
|
||||
},
|
||||
|
||||
"active": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -70,7 +70,8 @@
|
||||
"errorSendCode": "ارسال کد با خطا مواجه شد",
|
||||
"phoneVerified": "تلفن همراه تایید شد",
|
||||
"errorConfirmCode": "تایید کد با خطا مواجه شد",
|
||||
"errorChangePhone": "تغییر تلفن همراه با خطا مواجه شد"
|
||||
"errorChangePhone": "تغییر تلفن همراه با خطا مواجه شد",
|
||||
"verificationCodeSent": "کد تایید ارسال شد"
|
||||
},
|
||||
|
||||
"active": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
DatePicker,
|
||||
PickersDay,
|
||||
@@ -13,7 +13,6 @@ import { getDay } from 'date-fns-jalali';
|
||||
import { format as formatJalali } from 'date-fns-jalali';
|
||||
import { format } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
import { type DateOfBirthProps } from '../../types/settingForm';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { Calendar } from 'iconsax-react';
|
||||
@@ -21,6 +20,7 @@ import { Calendar } from 'iconsax-react';
|
||||
export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
const isFarsi = i18n.language === 'fa' || i18n.language === 'fa-IR';
|
||||
const [openView, setOpenView] = useState<'year' | 'month' | 'day'>('year');
|
||||
|
||||
const { Adapter, locale, formatString, dayOfWeekFormatter } = useMemo(() => {
|
||||
if (isFarsi) {
|
||||
@@ -44,11 +44,7 @@ export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
const dayNumber = isFarsi
|
||||
? formatJalali(props.day, 'dd')
|
||||
: format(props.day, 'dd');
|
||||
return (
|
||||
<PickersDay {...props}>
|
||||
{toLocaleDigits(dayNumber, i18n.language)}
|
||||
</PickersDay>
|
||||
);
|
||||
return <PickersDay {...props}>{dayNumber}</PickersDay>;
|
||||
};
|
||||
|
||||
const CustomCalendarIcon = () => (
|
||||
@@ -58,23 +54,23 @@ export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={Adapter} adapterLocale={locale}>
|
||||
<DatePicker
|
||||
label={toLocaleDigits(t('completion.dateOfBirth'), i18n.language)}
|
||||
label={t('completion.dateOfBirth')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
format={formatString}
|
||||
dayOfWeekFormatter={dayOfWeekFormatter}
|
||||
slots={{ day: CustomDay, openPickerIcon: CustomCalendarIcon }}
|
||||
disableFuture
|
||||
openTo="year"
|
||||
views={['year', 'month', 'day']}
|
||||
view={openView}
|
||||
onViewChange={(newView) => setOpenView(newView)}
|
||||
onYearChange={() => setOpenView('month')}
|
||||
slotProps={{
|
||||
textField: {
|
||||
fullWidth: true,
|
||||
},
|
||||
textField: { fullWidth: true },
|
||||
popper: {
|
||||
sx: {
|
||||
'& .MuiDateCalendar-root': {
|
||||
// TODO: fix this to use textfield width instead of defining hardcode
|
||||
width: '309px',
|
||||
},
|
||||
'& .MuiDateCalendar-root': { width: '309px' },
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
TextField,
|
||||
Box,
|
||||
@@ -13,30 +13,35 @@ import { useTranslation } from 'react-i18next';
|
||||
import { TickCircle, Edit2 } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type EmailSectionProps } from '../../types/settingForm';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
import { sanitizeLocalNumber } from '@/utils/regexes/sanitizeNumber';
|
||||
|
||||
export function EmailSection({
|
||||
showEmail,
|
||||
setShowEmail,
|
||||
email,
|
||||
setEmail,
|
||||
codeSent,
|
||||
verificationCode,
|
||||
setVerificationCode,
|
||||
buttonState,
|
||||
handleSendCode,
|
||||
handleVerifyCode,
|
||||
emailVerified,
|
||||
isVerifyingCode,
|
||||
isSendingCode,
|
||||
handleEditEmail,
|
||||
errors,
|
||||
touched,
|
||||
handleBlur,
|
||||
countdown,
|
||||
}: EmailSectionProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
const ADORN_W = 24;
|
||||
const INPUT_H = 56;
|
||||
|
||||
export function EmailSection(props: EmailSectionProps) {
|
||||
const {
|
||||
showEmail,
|
||||
setShowEmail,
|
||||
email,
|
||||
setEmail,
|
||||
codeSent,
|
||||
verificationCode,
|
||||
setVerificationCode,
|
||||
buttonState,
|
||||
handleSendCode,
|
||||
handleVerifyCode,
|
||||
emailVerified,
|
||||
isVerifyingCode,
|
||||
isSendingCode,
|
||||
handleEditEmail,
|
||||
errors,
|
||||
touched,
|
||||
handleBlur,
|
||||
countdown,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('completionForm');
|
||||
|
||||
const handleToggleEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShowEmail(e.target.checked);
|
||||
@@ -45,29 +50,31 @@ export function EmailSection({
|
||||
const handleVerificationCodeChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const value = e.target.value;
|
||||
// Allow only digits and enforce the max length
|
||||
if (isNumeric(value) && value.length <= 4) {
|
||||
setVerificationCode(value);
|
||||
const normalized = sanitizeLocalNumber(e.target.value);
|
||||
if (isNumeric(normalized) && normalized.length <= 4) {
|
||||
setVerificationCode(normalized);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTimerValue = () => {
|
||||
const m = String(Math.floor(countdown / 60)).padStart(2, '0');
|
||||
const s = String(countdown % 60).padStart(2, '0');
|
||||
return toLocaleDigits(`${m}:${s}`, i18n.language);
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (buttonState !== 'counting') {
|
||||
setVerificationCode('');
|
||||
}
|
||||
}, [buttonState, setVerificationCode]);
|
||||
|
||||
const showCodeSection =
|
||||
showEmail && codeSent && !emailVerified && buttonState === 'counting';
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Switch checked={showEmail} onChange={handleToggleEmail} />
|
||||
<Typography
|
||||
sx={{ color: showEmail ? 'primary.main' : 'text.primary' }}
|
||||
@@ -76,14 +83,9 @@ export function EmailSection({
|
||||
</Typography>
|
||||
</Box>
|
||||
</FormGroup>
|
||||
|
||||
{showEmail && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -97,40 +99,75 @@ export function EmailSection({
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={email}
|
||||
autoFocus
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onBlur={() => handleBlur('email')}
|
||||
disabled={isSendingCode || codeSent}
|
||||
error={touched.email && !!errors.email}
|
||||
helperText={touched.email && errors.email}
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
sx={{
|
||||
flex: '1 1 260px',
|
||||
'& .MuiOutlinedInput-root': { height: INPUT_H },
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment:
|
||||
!isVerifyingCode && emailVerified ? (
|
||||
<InputAdornment position="start">
|
||||
<Icon
|
||||
Component={TickCircle}
|
||||
size="medium"
|
||||
variant="Bold"
|
||||
color="success.main"
|
||||
/>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
endAdornment: codeSent ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleEditEmail}>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
size="medium"
|
||||
/>
|
||||
</IconButton>
|
||||
startAdornment: (
|
||||
<InputAdornment position="start" sx={{ width: ADORN_W }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: ADORN_W,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
visibility:
|
||||
!isVerifyingCode && emailVerified
|
||||
? 'visible'
|
||||
: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
Component={TickCircle}
|
||||
size="medium"
|
||||
variant="Bold"
|
||||
color="success.main"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end" sx={{ width: ADORN_W }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: ADORN_W,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ visibility: codeSent ? 'visible' : 'hidden' }}
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleEditEmail}
|
||||
disabled={!codeSent}
|
||||
>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
size="medium"
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isVerifyingCode &&
|
||||
!emailVerified &&
|
||||
(buttonState === 'counting' ? (
|
||||
@@ -140,6 +177,8 @@ export function EmailSection({
|
||||
width: { xs: '100%', sm: '156px' },
|
||||
alignSelf: 'center',
|
||||
textAlign: 'center',
|
||||
lineHeight: `${INPUT_H}px`,
|
||||
height: INPUT_H,
|
||||
}}
|
||||
color="primary"
|
||||
variant="body1"
|
||||
@@ -154,41 +193,53 @@ export function EmailSection({
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '156px' },
|
||||
alignSelf: 'center',
|
||||
height: INPUT_H,
|
||||
}}
|
||||
>
|
||||
{t('completion.vericationCodeButton')}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
{!emailVerified && codeSent && (
|
||||
|
||||
{showCodeSection && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label={t('completion.verificationCode')}
|
||||
autoFocus
|
||||
variant="outlined"
|
||||
type="text"
|
||||
value={verificationCode}
|
||||
onChange={handleVerificationCodeChange}
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
sx={{
|
||||
flex: '1 1 260px',
|
||||
'& .MuiOutlinedInput-root': { height: INPUT_H },
|
||||
}}
|
||||
disabled={isVerifyingCode}
|
||||
onBlur={() => handleBlur('verificationCode')}
|
||||
error={touched.verificationCode && !!errors.verificationCode}
|
||||
helperText={touched.verificationCode && errors.verificationCode}
|
||||
inputProps={{
|
||||
inputMode: 'numeric',
|
||||
pattern: '[0-9]*',
|
||||
maxLength: 4,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleVerifyCode}
|
||||
loading={isVerifyingCode}
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '156px' },
|
||||
alignSelf: 'center',
|
||||
alignSelf: 'stretch',
|
||||
height: INPUT_H,
|
||||
}}
|
||||
>
|
||||
{t('completion.checkCodeButton')}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type PersonalInfoFieldsProps } from '../../types/settingForm';
|
||||
import { Gender } from '../../types/settingForm';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { sanitizeLocalNumber } from '@/utils/regexes/sanitizeNumber';
|
||||
|
||||
export function PersonalInfoFields({
|
||||
firstName,
|
||||
@@ -129,6 +130,7 @@ export function PersonalInfoFields({
|
||||
value={currentCountry}
|
||||
onChange={(_, newValue) => setCountry(newValue?.code || '')}
|
||||
onBlur={() => handleBlur('country')}
|
||||
autoFocus
|
||||
renderOption={(props, option) => (
|
||||
<Box component="li" {...props} key={option.code}>
|
||||
<ReactCountryFlag
|
||||
@@ -163,7 +165,13 @@ export function PersonalInfoFields({
|
||||
label={t('completion.optionalNationalCode')}
|
||||
placeholder={t('completion.optionalNationalCode')}
|
||||
value={nationalId}
|
||||
onChange={(e) => setNationalId(e.target.value)}
|
||||
onChange={(e) => {
|
||||
const normalized = sanitizeLocalNumber(e.target.value);
|
||||
|
||||
if (normalized.length <= 10) {
|
||||
setNationalId(normalized);
|
||||
}
|
||||
}}
|
||||
variant="outlined"
|
||||
onBlur={() => handleBlur('nationalId')}
|
||||
error={touched.nationalId && !!errors.nationalId}
|
||||
|
||||
@@ -7,9 +7,12 @@ import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { type SubmitProps } from '../../types/settingForm';
|
||||
import { CloseCircle } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
|
||||
export function SubmitSection({ loading }: SubmitProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
@@ -21,6 +24,7 @@ export function SubmitSection({ loading }: SubmitProps) {
|
||||
};
|
||||
|
||||
const agreementText = t('completion.agreement');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
@@ -72,7 +76,16 @@ export function SubmitSection({ loading }: SubmitProps) {
|
||||
maxWidth="md"
|
||||
dir={i18n.language.startsWith('fa') ? 'rtl' : 'ltr'}
|
||||
>
|
||||
<DialogTitle>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={() => setOpenDialog(false)}>
|
||||
<Icon Component={CloseCircle} size="medium" color="primary.main" />
|
||||
</IconButton>
|
||||
{t('completion.rules') || t('completion.rules')}
|
||||
</DialogTitle>
|
||||
|
||||
|
||||
@@ -64,10 +64,8 @@ export function UserCompletionPage() {
|
||||
const correctEmail = isEmail(email);
|
||||
|
||||
const { execute: sendCode, loading: isSendingCode } = useApi(sendEmailOtpApi);
|
||||
|
||||
const { execute: verifyCode, loading: isVerifyingCode } =
|
||||
useApi(confirmEmailOtpApi);
|
||||
|
||||
const { execute: submitForm, loading: isSubmitting } = useApi(
|
||||
completeUserInformationApi,
|
||||
);
|
||||
@@ -89,13 +87,30 @@ export function UserCompletionPage() {
|
||||
return () => clearInterval(timer);
|
||||
}, [buttonState, countdown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (buttonState === 'default') {
|
||||
setCodeSent(false);
|
||||
setVerificationCode('');
|
||||
// setEmailVerified(false);
|
||||
setTouched((prev) => {
|
||||
const n = { ...prev };
|
||||
delete n.verificationCode;
|
||||
return n;
|
||||
});
|
||||
setErrors((prev) => {
|
||||
const n = { ...prev };
|
||||
delete n.verificationCode;
|
||||
return n;
|
||||
});
|
||||
}
|
||||
}, [buttonState]);
|
||||
|
||||
const handleSendCode = async () => {
|
||||
if (!isEmail(email)) {
|
||||
setTouched((prev) => ({ ...prev, email: true }));
|
||||
setErrors((prev) => ({ ...prev, email: t('validation.emailInvalid') }));
|
||||
return;
|
||||
}
|
||||
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.verificationCode;
|
||||
@@ -106,13 +121,11 @@ export function UserCompletionPage() {
|
||||
delete newErrors.verificationCode;
|
||||
return newErrors;
|
||||
});
|
||||
|
||||
const res = await sendCode({ email });
|
||||
|
||||
if (res) {
|
||||
if (res.success) {
|
||||
showToast({
|
||||
message: res.message || t('completion.successfulCodeSent'),
|
||||
message: t('completion.successfulCodeSent'),
|
||||
severity: 'success',
|
||||
});
|
||||
setCodeSent(true);
|
||||
@@ -129,8 +142,6 @@ export function UserCompletionPage() {
|
||||
|
||||
const handleVerifyCode = async () => {
|
||||
const trimmedCode = verificationCode.trim();
|
||||
|
||||
// Manually trigger the validation error and stop
|
||||
if (!trimmedCode) {
|
||||
setTouched((prev) => ({ ...prev, verificationCode: true }));
|
||||
setErrors((prev) => ({
|
||||
@@ -146,9 +157,7 @@ export function UserCompletionPage() {
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await verifyCode({ email, otpCode: verificationCode });
|
||||
|
||||
if (res) {
|
||||
if (res.success) {
|
||||
setEmailVerified(true);
|
||||
@@ -178,12 +187,10 @@ export function UserCompletionPage() {
|
||||
confirmPassword: showPasswordSection,
|
||||
sex: true,
|
||||
});
|
||||
|
||||
const isValid = validateForm();
|
||||
if (!isValid) {
|
||||
return; // Stop the submission
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await submitForm({
|
||||
firstName,
|
||||
lastName,
|
||||
@@ -196,16 +203,13 @@ export function UserCompletionPage() {
|
||||
birthDate,
|
||||
countryCode: country,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
if (res.success) {
|
||||
showToast({
|
||||
message: res.message || t('completion.submitSuccess'),
|
||||
severity: 'success',
|
||||
});
|
||||
|
||||
const returnUrl = params.get('returnUrl');
|
||||
|
||||
navigate(
|
||||
returnUrl ? `/account-created?returnUrl=${returnUrl}` : '/setting',
|
||||
);
|
||||
@@ -223,7 +227,6 @@ export function UserCompletionPage() {
|
||||
setCodeSent(false);
|
||||
setEmailVerified(false);
|
||||
setVerificationCode('');
|
||||
// We clear both touched and errors
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.email;
|
||||
@@ -240,77 +243,49 @@ export function UserCompletionPage() {
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: { [key: string]: string } = {};
|
||||
|
||||
// Rule 1: First Name is required
|
||||
if (!firstName.trim())
|
||||
newErrors.firstName = t('validation.firstNameRequired');
|
||||
|
||||
// Rule 2: Last Name is required
|
||||
if (!lastName.trim()) newErrors.lastName = t('validation.lastNameRequired');
|
||||
|
||||
// Rule 3: Country is required
|
||||
if (!country) newErrors.country = t('validation.countryRequired');
|
||||
|
||||
// Rule 4: Email is required and must be valid IF the section is shown
|
||||
if (showEmail && !isEmail(email)) {
|
||||
newErrors.email = t('validation.emailInvalid');
|
||||
}
|
||||
|
||||
// Rule 5: National ID must be 10 digits IF it's not empty
|
||||
if (nationalId && !nationalIdRegex.test(nationalId)) {
|
||||
newErrors.nationalId = t('validation.nationalIdInvalid');
|
||||
}
|
||||
|
||||
// Rule 6: If verification code sent and email section is active
|
||||
if (showEmail && codeSent && !emailVerified) {
|
||||
const trimmedCode = verificationCode.trim();
|
||||
|
||||
if (!trimmedCode) {
|
||||
// Case 1: The code is required but the field is empty.
|
||||
newErrors.verificationCode = t('validation.verificationCodeRequired');
|
||||
} else if (trimmedCode.length < 4) {
|
||||
// Case 2 : The code is entered but is less than 4 digits.
|
||||
newErrors.verificationCode = t('validation.verificationCodeInvalid');
|
||||
} else {
|
||||
// Case 3: The user has typed a code but hasn't clicked "Verify" yet.
|
||||
newErrors.verificationCode = t('validation.mustVerifyCode');
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 7: Password validation
|
||||
if (showPasswordSection) {
|
||||
// Rule 1: Check if the main password is valid
|
||||
if (!password.trim()) {
|
||||
newErrors.password = t('validation.passwordRequired');
|
||||
} else if (!validPassword) {
|
||||
// 'validPassword' is the boolean you already calculate
|
||||
newErrors.password = t('validation.passwordInvalid');
|
||||
}
|
||||
|
||||
// Rule 2: Check if the confirmation password matches
|
||||
if (!confirmPassword.trim()) {
|
||||
newErrors.confirmPassword = t('validation.confirmPasswordRequired');
|
||||
} else if (!matchPassword) {
|
||||
// 'matchPassword' is the boolean you already calculate
|
||||
newErrors.confirmPassword = t('validation.passwordsDoNotMatch');
|
||||
}
|
||||
}
|
||||
|
||||
if (sex === null) {
|
||||
newErrors.sex = t('validation.genderRequired');
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0; // Returns true if form is valid
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleBlur = (field: string) => {
|
||||
setTouched((prev) => ({ ...prev, [field]: true }));
|
||||
};
|
||||
const handleBlur = (_field: string) => {};
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPasswordSection) {
|
||||
// We clear both touched and errors to prevent lingering validation messages
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.password;
|
||||
@@ -326,10 +301,8 @@ export function UserCompletionPage() {
|
||||
}
|
||||
}, [showPasswordSection]);
|
||||
|
||||
// This effect resets email fields when the section is hidden
|
||||
useEffect(() => {
|
||||
if (!showEmail) {
|
||||
// We clear both touched and errors
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.email;
|
||||
@@ -345,9 +318,7 @@ export function UserCompletionPage() {
|
||||
}
|
||||
}, [showEmail]);
|
||||
|
||||
// re-validate whenever a field the user has touched changes value
|
||||
useEffect(() => {
|
||||
// Only run validation if at least one field has been touched
|
||||
if (Object.keys(touched).length > 0) {
|
||||
validateForm();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useToast } from '@rkheftan/harmony-ui';
|
||||
import { useProfile } from '../../hooks/useProfile';
|
||||
|
||||
export function PersonalInformation() {
|
||||
const imageBaseUrl = import.meta.env.IMAGE_BASE_URL;
|
||||
const imageBaseUrl = import.meta.env.VITE_IMAGE_BASE_URL;
|
||||
const { t } = useTranslation('setting');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
|
||||
@@ -50,9 +50,10 @@ export function PersonalInformation() {
|
||||
setOriginalData(fetchedData);
|
||||
setUploadedImageUrl(
|
||||
profileData.profileImageUrl
|
||||
? `${imageBaseUrl}${profileData.profileImageUrl}`
|
||||
? `${imageBaseUrl}/${profileData.profileImageUrl}`
|
||||
: null,
|
||||
);
|
||||
console.log(uploadedImageUrl);
|
||||
setUploadedImageFile(null);
|
||||
} else {
|
||||
showToast({
|
||||
|
||||
@@ -103,7 +103,7 @@ export function SocialMedia() {
|
||||
|
||||
if (sendCodeData.success) {
|
||||
toast({
|
||||
message: sendCodeData.message || t('settingForm.verificationCodeSent'),
|
||||
message: t('settingForm.verificationCodeSent'),
|
||||
severity: 'success',
|
||||
});
|
||||
setDialogStep('enterCode');
|
||||
|
||||
@@ -126,7 +126,7 @@ export default function SocialMediaDialog({
|
||||
placeholder="abc@email.com"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1 }, mt: 1 }}
|
||||
autoFocus={dialogStep === 'enterEmail'}
|
||||
disabled={isLoading || dialogStep === 'enterCode'}
|
||||
error={touched && !!emailError}
|
||||
@@ -142,7 +142,7 @@ export default function SocialMediaDialog({
|
||||
label={t('settingForm.verificationCode')}
|
||||
autoComplete="one-time-code"
|
||||
inputMode="numeric"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1 } }}
|
||||
autoFocus
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -152,7 +152,7 @@ export default function SocialMediaDialog({
|
||||
<Box sx={{ px: 3, pb: 2 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{ height: 48, textTransform: 'none', borderRadius: 2 }}
|
||||
sx={{ height: 48, textTransform: 'none', borderRadius: 1 }}
|
||||
variant="contained"
|
||||
type="submit"
|
||||
disabled={isLoading || (dialogStep === 'enterEmail' && !emailInput)}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { Message, Google, Apple, ArrowDown3 } from 'iconsax-react';
|
||||
import { Message, Google, ArrowDown3 } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type SocialMediaMenuProps } from '@/features/profile/types/settingsType';
|
||||
|
||||
@@ -61,9 +61,9 @@ export default function SocialMediaMenu({
|
||||
<Box component="span">{t('settingForm.addEmailOrSocialButton')}</Box>
|
||||
<Icon
|
||||
Component={ArrowDown3}
|
||||
size="medium"
|
||||
size="small"
|
||||
color="primary.main"
|
||||
variant={open ? 'Bold' : 'Outline'}
|
||||
variant="Outline"
|
||||
/>
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -98,12 +98,6 @@ export default function SocialMediaMenu({
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.google')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<Icon Component={Apple} size="medium" color="primary.main" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.apple')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
|
||||
9
src/utils/regexes/normalizeDigits.ts
Normal file
9
src/utils/regexes/normalizeDigits.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
const PERSIAN = '۰۱۲۳۴۵۶۷۸۹';
|
||||
const ARABIC = '٠١٢٣٤٥٦٧٨٩';
|
||||
export const normalizeDigits = (str: string) =>
|
||||
str.replace(/[\u06F0-\u06F9\u0660-\u0669]/g, (d) => {
|
||||
const iP = PERSIAN.indexOf(d);
|
||||
if (iP !== -1) return String(iP);
|
||||
const iA = ARABIC.indexOf(d);
|
||||
return iA !== -1 ? String(iA) : d;
|
||||
});
|
||||
4
src/utils/regexes/sanitizeNumber.ts
Normal file
4
src/utils/regexes/sanitizeNumber.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { normalizeDigits } from './normalizeDigits';
|
||||
|
||||
export const sanitizeLocalNumber = (v: string) =>
|
||||
normalizeDigits(v).replace(/\D+/g, '');
|
||||
Reference in New Issue
Block a user