diff --git a/.env b/.env index 28623f6..08da216 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ VITE_GOOGLE_CLIENT_ID=272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com -VITE_DEFUALT_AUTH_RETURN_URL=/setting/profile +VITE_DEFAULT_AUTH_RETURN_URL=/setting/profile VITE_APP_URL=https://accounts.business-harmony.com VITE_API_URL=https://accounts.business-harmony.com/api VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token diff --git a/package.json b/package.json index 960f5ec..65954f7 100644 --- a/package.json +++ b/package.json @@ -58,4 +58,4 @@ "typescript-eslint": "^8.34.1", "vite": "^7.0.0" } -} +} \ No newline at end of file diff --git a/public/locales/en/authentication.json b/public/locales/en/authentication.json index 9265dc5..31375ae 100644 --- a/public/locales/en/authentication.json +++ b/public/locales/en/authentication.json @@ -9,8 +9,8 @@ "phoneNumberIsInvalid": "Phone number is invalid", "thisFieldIsRequired": "This field is required", "googleAuthenticationFailed": "Login with google failed", - "persian": "Persian(Fa)", - "english": "English(En)", + "persian": "Persian (Fa)", + "english": "English (En)", "accountInfo": "Harmony Account - 2025" }, "verify": { diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json index 5c09ec3..21eb655 100644 --- a/public/locales/fa/authentication.json +++ b/public/locales/fa/authentication.json @@ -9,8 +9,8 @@ "phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد", "thisFieldIsRequired": "این فیلد الزامی است", "googleAuthenticationFailed": "ورود با گوگل با خطا مواجه شد", - "persian": "فارسی(Fa)", - "english": "انگلیسی(En)", + "persian": "فارسی (Fa)", + "english": "انگلیسی (En)", "accountInfo": "۱۴۰۴-هارمونی اکانت" }, "verify": { diff --git a/src/components/DigitsInput.tsx b/src/components/DigitsInput.tsx index d38f67f..4e33a06 100644 --- a/src/components/DigitsInput.tsx +++ b/src/components/DigitsInput.tsx @@ -57,10 +57,20 @@ const DigitInput: React.FC = ({ event: KeyboardEvent, index: number, ) => { - if (event.key === 'Backspace' && code[index]) { + if (event.key === 'Backspace') { event.preventDefault(); - handleChange('', index); - if (index > 0) inputRefs.current[index - 1]?.focus(); + + if (code[index]) { + // We clear the current value. + handleChange('', index); + } else if (index > 0) { + // We move focus to the previous input and SELECT its content. + const prevInput = inputRefs.current[index - 1]; + if (prevInput) { + prevInput.focus(); + prevInput.select(); + } + } } }; diff --git a/src/features/authentication/api/authorizationAPI.ts b/src/features/authentication/api/authorizationAPI.ts index 2e62c71..b1bb4d9 100644 --- a/src/features/authentication/api/authorizationAPI.ts +++ b/src/features/authentication/api/authorizationAPI.ts @@ -16,6 +16,7 @@ import type { SendEmailOtpRequest, SendForgetPassCodeRequest, SendSmsOtpRequest, + SendSmsOtpResponse, } from '../types/userTypes'; import apiClient from '@/lib/apiClient'; @@ -39,15 +40,29 @@ export const loginWithPassword = async (body: PasswordLoginRequest) => { }; export const sendSmsOtp = async (body: SendSmsOtpRequest) => { - return apiClient.post('User/SendSmsOtp', body); + return apiClient.post('User/SendSmsOtp', body); }; export const sendEmailOtp = async (body: SendEmailOtpRequest) => { return apiClient.post('User/SendEmailOtp', body); }; -export const confirmSmsOtp = async (body: ConfirmSmsOtpRequest) => { - return apiClient.post('User/ConfirmSmsOtp', body); +export const sendSmsCodeCompleteUserInforamation = async ( + body: SendSmsOtpRequest, +) => { + return apiClient.post( + 'User/SendSmsCodeCompleteUserInforamation', + body, + ); +}; + +export const confirmSmsCodeCompleteUserInforamation = async ( + body: ConfirmSmsOtpRequest, +) => { + return apiClient.post( + 'User/ConfirmSmsCodeCompleteUserInforamation', + body, + ); }; export const confirmEmailOtp = async (body: ConfirmEmailOtpRequest) => { diff --git a/src/features/authentication/components/AuthenticationCard.tsx b/src/features/authentication/components/AuthenticationCard.tsx index 20b72a3..c9c060c 100644 --- a/src/features/authentication/components/AuthenticationCard.tsx +++ b/src/features/authentication/components/AuthenticationCard.tsx @@ -6,7 +6,6 @@ export interface AuthenticationCardProps extends PropsWithChildren { sx?: SxProps; } -// Beacuse in the otp verify there is a element outside of the authentication card export const AuthenticationCard = ({ children, maxWidth, diff --git a/src/features/authentication/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authentication/components/AuthenticationSteps/AuthenticationSteps.tsx index f394e52..0f5991d 100644 --- a/src/features/authentication/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authentication/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -31,6 +31,7 @@ export const AuthenticationSteps = (): JSX.Element => { useState(''); const [memoryTokenRes, setMemoryTokenRes] = useState(); const [hasPassword, setHasPassword] = useState(false); + const [timerValue, setTimerValue] = useState(120); const { login } = useAuth(); const authFactory: AuthFactory = useMemo(() => { @@ -66,8 +67,13 @@ export const AuthenticationSteps = (): JSX.Element => { return resFactory; }, [searchParams]); - const handleLoginRegister = (value: string, userStatus: UserStatus) => { + const handleLoginRegister = ( + value: string, + userStatus: UserStatus, + timerValue: number, + ) => { setAuthType(isNumeric(value) ? 'phone' : 'email'); + setTimerValue(timerValue); switch (userStatus) { case UserStatus.NotRegistered: @@ -131,7 +137,7 @@ export const AuthenticationSteps = (): JSX.Element => { const redirectToReturnUrl = (refreshToken: string) => { if (authFactory.isCurrentApplication()) { - navigate(import.meta.env.VITE_DEFUALT_AUTH_RETURN_URL); + navigate(import.meta.env.VITE_DEFAULT_AUTH_RETURN_URL); } else { if (authMode === 'register') { navigate( @@ -169,6 +175,7 @@ export const AuthenticationSteps = (): JSX.Element => { authType={authType} value={loginRegisterValue} hasPassword={hasPassword} + initialTimerValue={timerValue} onLoginWithPassword={() => setCurrentStep('enterPassword')} /> )} @@ -194,6 +201,7 @@ export const AuthenticationSteps = (): JSX.Element => { setValue={setAddedPhoneNumberValue} email={loginRegisterValue} onCompleteSignUp={() => setCurrentStep('addedPhoneNumberVerify')} + setTimerValue={setTimerValue} /> )} @@ -203,6 +211,7 @@ export const AuthenticationSteps = (): JSX.Element => { onEditValue={() => setCurrentStep('addPhoneNumber')} value={addedPhoneNumberValue} onPhoneNumberVerified={handlePhoneNumberVerified} + initialTimerValue={timerValue} /> )} diff --git a/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx b/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx index 159ed01..872dd88 100644 --- a/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx +++ b/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx @@ -4,10 +4,14 @@ import { useRef, useState, type ChangeEvent, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; import { AuthenticationCard } from '../AuthenticationCard'; import { CountryCodeSelector } from '../../../../components/CountryCodeSelector'; -import { sendSmsOtp } from '../../api/authorizationAPI'; +import { + sendSmsCodeCompleteUserInforamation, + sendSmsOtp, +} from '../../api/authorizationAPI'; import type { CountryCode } from '@/types/commonTypes'; import { useApi } from '@/hooks/useApi'; import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers'; +import { useToast } from '@rkheftan/harmony-ui'; export interface CompleteSignUpProps { email: string; @@ -16,6 +20,7 @@ export interface CompleteSignUpProps { countryCode: CountryCode; setCountryCode: Dispatch; onCompleteSignUp: (countryCode: string, value: string) => void; + setTimerValue: Dispatch; } export const CompleteSignUp = ({ @@ -25,6 +30,7 @@ export const CompleteSignUp = ({ countryCode, setCountryCode, onCompleteSignUp, + setTimerValue, }: CompleteSignUpProps) => { const { t, i18n } = useTranslation('authentication'); const [error, setError] = useState(); @@ -32,7 +38,10 @@ export const CompleteSignUp = ({ const inputRef = useRef(null); const [touched, setTouched] = useState(false); const inputError: boolean = touched && !!error; - const { loading: sendSmsLoading, execute: sendSmsCall } = useApi(sendSmsOtp); + const { loading: sendSmsLoading, execute: sendSmsCall } = useApi( + sendSmsCodeCompleteUserInforamation, + ); + const toast = useToast(); const isPhoneValid = (code: string, phone: string) => { const phoneNumber = parsePhoneNumberFromString(code + phone); @@ -74,8 +83,20 @@ export const CompleteSignUp = ({ newValue = newValue.substring(1); setValue(newValue); } - await sendSmsCall({ phoneNumber: countryCode + newValue }); - onCompleteSignUp(countryCode, newValue); + const res = await sendSmsCall({ phoneNumber: countryCode + newValue }); + + if (!res) return; + + if (res.success) { + onCompleteSignUp(countryCode, newValue); + setTimerValue(res.totalSecondForCodeToExpire); + } else { + toast({ + message: res.message, + severity: 'error', + }); + setError(res.message); + } } }; diff --git a/src/features/authentication/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authentication/components/AuthenticationSteps/EnterPasswordForm.tsx index 6080abd..0b7863c 100644 --- a/src/features/authentication/components/AuthenticationSteps/EnterPasswordForm.tsx +++ b/src/features/authentication/components/AuthenticationSteps/EnterPasswordForm.tsx @@ -133,7 +133,7 @@ export const EnterPasswordForm = ({ - ); -}; diff --git a/src/features/authentication/components/AuthenticationSteps/GoogleAuthenticationV2.tsx b/src/features/authentication/components/AuthenticationSteps/GoogleAuthenticationV2.tsx index a6f447c..44dd66e 100644 --- a/src/features/authentication/components/AuthenticationSteps/GoogleAuthenticationV2.tsx +++ b/src/features/authentication/components/AuthenticationSteps/GoogleAuthenticationV2.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import type { LoginOrSignUpWithGoogleRequest, LoginResult, @@ -12,7 +12,7 @@ import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI'; import { useApi } from '@/hooks/useApi'; import { useToast } from '@rkheftan/harmony-ui'; import { useTranslation } from 'react-i18next'; -import { Box } from '@mui/material'; +import { Box, Button } from '@mui/material'; interface GoogleAuthenticationV2Props { authFactory: AuthFactory; @@ -29,6 +29,7 @@ export const GoogleAuthenticationV2 = ({ const toast = useToast(); const { t } = useTranslation('authentication'); const { execute: loginWithGoogleCall } = useApi(loginOrSignUpWithGoogle); + const [isGoogleLoaded, setIsGoogleLoaded] = useState(false); const googleBtnRef = useRef(null); const renderedRef = useRef(false); @@ -74,6 +75,8 @@ export const GoogleAuthenticationV2 = ({ const initializeGoogle = () => { if (!window.google) return; + setIsGoogleLoaded(true); + google.accounts.id.initialize({ client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID, callback: handleCredentialResponse, @@ -85,8 +88,9 @@ export const GoogleAuthenticationV2 = ({ type: 'standard', // The standard "Sign in with Google" button text: 'signin_with', // Text: "Sign in with Google" shape: 'rectangular', // Matches your MUI button shape - width: '456', // Set a width (Google caps this at 400px) + width: '400', // Set a width (Google caps this at 400px) logo_alignment: 'left', + height: '44', }); @@ -116,6 +120,7 @@ export const GoogleAuthenticationV2 = ({ minHeight: 44, }} > + {!isGoogleLoaded && diff --git a/src/features/authentication/components/UserCompletionForm/EmailSection.tsx b/src/features/authentication/components/UserCompletionForm/EmailSection.tsx index 702901e..9260eac 100644 --- a/src/features/authentication/components/UserCompletionForm/EmailSection.tsx +++ b/src/features/authentication/components/UserCompletionForm/EmailSection.tsx @@ -110,22 +110,14 @@ export function EmailSection(props: EmailSectionProps) { }} slotProps={{ input: { - startAdornment: ( - - + startAdornment: + !isVerifyingCode && emailVerified ? ( + - - - ), - endAdornment: ( + + ) : undefined, + endAdornment: codeSent ? ( - - - - - + + - ), + ) : undefined, }, }} /> diff --git a/src/features/authentication/routes/AuthenticationPage.tsx b/src/features/authentication/routes/AuthenticationPage.tsx index 8104bbc..78141c6 100644 --- a/src/features/authentication/routes/AuthenticationPage.tsx +++ b/src/features/authentication/routes/AuthenticationPage.tsx @@ -9,7 +9,7 @@ export function AuthenticationPage() { align="center" justify="center" sx={{ - minHeight: '100vh', + minHeight: '100dvh', gap: 3, }} > diff --git a/src/features/authentication/types/userTypes.ts b/src/features/authentication/types/userTypes.ts index 44ecc84..125a852 100644 --- a/src/features/authentication/types/userTypes.ts +++ b/src/features/authentication/types/userTypes.ts @@ -10,6 +10,7 @@ export interface GetUserStatusByPhoneNumberOrEmailRequest { export interface GetUserStatusByPhoneNumberOrEmailResponse extends ApiResponse { userStatus: UserStatus; + totalSecondForOtpToExpire: number; } export enum UserStatus { @@ -50,6 +51,10 @@ export interface SendSmsOtpRequest { phoneNumber: string; } +export interface SendSmsOtpResponse extends ApiResponse { + totalSecondForCodeToExpire: number; +} + // SendEmailOtp export interface SendEmailOtpRequest {