From 70a51b993b5a0025b93ebc84fa9a41169a6b0521 Mon Sep 17 00:00:00 2001 From: bunsy-0900 <148200748+bunsy-0900@users.noreply.github.com> Date: Mon, 11 Dec 2023 14:41:47 +0700 Subject: [PATCH] ES-513: wiring signup UI with signup service endpoints (#31) * chore: change query hook from verb to noun * chore: adapt naming convention * chore: use tanstack query and dev tool * fix: fix registration type imports * fix: resolve validate dom nesting * feat: check account status * fix: remove duplicates * fix: control step with zustand and refactor structures * fix: remove sign up context * fix: fix ui of OTP step * fix: fix ui of Phone step * fix: fix deprecated css classes * feat: conditionally render font based on language * fix: show lang with corresponding font family * fix: redirect user back to callback url * fix: fix default input text * fix: fix phone status and account registration status steps * fix: fix ui of account setup status step * fix: translate setup account title and description * fix: adapt the layout of the design when scrolling * fix: add arrow to popover * fix: fix ul list disc breaking styling * feat: reset otp on resending otp * chore: add tailwindcss prettier * feat: add storybook * fix: fix styling and format * fix: fix button styles * feat: add stories for components * fix: remove pnpm lock * fix(AccountSetup): fix styles * fix(Phone): fix styles * fix(Phone): fix phone input * fix(Phone): fix phone input height * fix: use identifier prefix * fix(Phone): fix back icon margin * fix: change api env variable * chore(Readme): add storybook script * fix: add identifier prefix * fix(form): fix form validation msg design * fix(Step): fix alert in step * fix(Phone): fix challenge generation handler * fix(validation): add dynamic identifier prefix validation * fix(otp): add identifier prefix * fix(termpolicy): fix open term and policy modals * fix: handle account registration status in the next step * fix: fix otp step * fix(phone): use sitekey from api * fix: fix package duplication * fix: fix types * fix: pass locale and isRegenerate to api * fix: fix otp handler when phone already registered * fix: refactor resend delay * fix: add loading state to account setup submit * fix: refactor otp handler * fix: reset error msg before resend otp * fix: fix redirect to sign in or sign up * fix: fix otp error msg * fix: add popup timeout * fix: handle account registration status * fix: fix language dropdown layer * fix: remove deprecated env variables * fix: rename PUBLIC_URL to BASE_URL * fix: use 3-character locale * fix: switch language priority * fix: map error code to related translation * fix: empty error response template * fix: fix account registration status conditions * fix: fix popup timeout * fix: redirect users on invalid transaction and timeout * fix: fix label * fix: reset input upon passing global error * fix: fix i18n language * fix: remove text * fix: fix otp timeout flow * fix: fix language initialization * fix: fix conditions handler * fix: refactor active message * fix: fix transaction timeout error flow * fix: fix number of time status checked * fix: set error message for request limit * fix: use only invalid_transaction * chore: remove deprecated env variable * fix: fix no action taken by user * fix: ignore garbage-collect * fix: fix generate challenge key --------- Co-authored-by: Bunsy Signed-off-by: Sreang Rathanak --- signup-ui/.env.example | 3 +- signup-ui/package.json | 2 +- signup-ui/public/env-config.js | 2 +- signup-ui/public/locales/default.json | 11 - signup-ui/public/locales/en.json | 16 +- signup-ui/public/locales/km.json | 14 + signup-ui/src/components/language.tsx | 35 +- .../src/components/ui/active-message.tsx | 27 ++ signup-ui/src/constants/language.ts | 12 + .../AccountRegistrationStatus.tsx | 127 ++++-- .../AccountRegistrationStatusLayout.tsx | 50 +++ .../SignUpPage/AccountSetup/AccountSetup.tsx | 53 ++- .../AccountSetupStatus/AccountSetupStatus.tsx | 57 ++- signup-ui/src/pages/SignUpPage/Otp/Otp.tsx | 375 ++++++++---------- .../src/pages/SignUpPage/Phone/Phone.tsx | 71 ++-- .../SignUpPage/PhoneStatus/PhoneStatus.tsx | 47 +++ signup-ui/src/pages/SignUpPage/SignUpPage.tsx | 42 +- .../src/pages/SignUpPage/SignUpPopover.tsx | 62 +++ signup-ui/src/pages/SignUpPage/mutations.ts | 10 + signup-ui/src/pages/SignUpPage/queries.ts | 16 +- signup-ui/src/pages/SignUpPage/service.ts | 2 +- .../src/pages/SignUpPage/useSignUpStore.tsx | 18 + signup-ui/src/resources.d.ts | 14 + signup-ui/src/services/i18n.service.ts | 3 + signup-ui/src/services/langConfig.service.tsx | 32 -- signup-ui/src/typings/types.ts | 18 +- signup-ui/src/utils/language.ts | 10 + signup-ui/src/utils/timer.ts | 12 + 28 files changed, 764 insertions(+), 377 deletions(-) delete mode 100644 signup-ui/public/locales/default.json create mode 100644 signup-ui/src/components/ui/active-message.tsx create mode 100644 signup-ui/src/pages/SignUpPage/AccountRegistrationStatus/components/AccountRegistrationStatusLayout.tsx create mode 100644 signup-ui/src/pages/SignUpPage/SignUpPopover.tsx delete mode 100644 signup-ui/src/services/langConfig.service.tsx create mode 100644 signup-ui/src/utils/language.ts diff --git a/signup-ui/.env.example b/signup-ui/.env.example index 4e2f03bc..96126858 100644 --- a/signup-ui/.env.example +++ b/signup-ui/.env.example @@ -1,4 +1,3 @@ -REACT_APP_API_BASE_URL="http://localhost:3333" -REACT_APP_CAPTCHA_SITE_KEY="" +REACT_APP_API_BASE_URL="http://localhost:8088/v1/signup" REACT_APP_REDIRECT_SIGN_IN_URL="https://esignet.camdgc-dev.mosip.net/authorize" REACT_APP_REDIRECT_SIGN_UP_URL="http://localhost:3000/signup" diff --git a/signup-ui/package.json b/signup-ui/package.json index 7287e284..ec4ce111 100644 --- a/signup-ui/package.json +++ b/signup-ui/package.json @@ -49,6 +49,7 @@ "react-router-dom": "^6.17.0", "react-scripts": "^5.0.1", "react-select": "^5.7.7", + "react-timer-hook": "^3.0.7", "react-tooltip": "^5.21.6", "rooks": "^7.14.1", "tailwind-merge": "^2.0.0", @@ -92,7 +93,6 @@ "@storybook/react-webpack5": "^7.6.0", "@storybook/test": "^7.6.0", "@tanstack/react-query-devtools": "^5.8.4", - "@tanstack/react-query-devtools": "^5.8.4", "@types/lodash": "^4.14.200", "@types/react-google-recaptcha": "^2.1.7", "autoprefixer": "^10.4.16", diff --git a/signup-ui/public/env-config.js b/signup-ui/public/env-config.js index b8d13f8e..e4bb0754 100644 --- a/signup-ui/public/env-config.js +++ b/signup-ui/public/env-config.js @@ -1,3 +1,3 @@ window._env_ = { - DEFAULT_LANG: "en", + DEFAULT_LANG: "km", }; diff --git a/signup-ui/public/locales/default.json b/signup-ui/public/locales/default.json deleted file mode 100644 index dbd1efd6..00000000 --- a/signup-ui/public/locales/default.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "languages_2Letters": { - "en": "English", - "km": "ខ្មែរ" - }, - "rtlLanguages": [], - "langCodeMapping": { - "eng": "en", - "khm": "km" - } -} diff --git a/signup-ui/public/locales/en.json b/signup-ui/public/locales/en.json index 91cb2ab3..93e12887 100644 --- a/signup-ui/public/locales/en.json +++ b/signup-ui/public/locales/en.json @@ -18,7 +18,7 @@ "complete_your_registration": "Please enter the requested details to complete your registration.", "username": "Username", "username_placeholder": "Enter username", - "full_name": "Full Name", + "full_name": "Full Name in Khmer", "full_name_placeholder": "Enter Full Name in Khmer", "full_name_tooltip": "Maximum 30 characters allowed and it should not contain any digit or special characters except “ “ space.", "password": "Password", @@ -52,5 +52,19 @@ "privacy_and_policy_title": "Privacy & Policy", "footer": { "powered_by": "Powered by" + }, + "error_response": { + "invalid_transaction": "", + "invalid_otp_channel": "", + "invalid_captcha": "", + "send_otp_failed": "", + "active_otp_found": "", + "unknown_error": "", + "challenge_failed": "", + "invalid_challenge_type": "", + "invalid_challenge_format": "", + "already-registered": "", + "timed_out": "", + "request_limit": "" } } diff --git a/signup-ui/public/locales/km.json b/signup-ui/public/locales/km.json index 3f5c0a5c..d7ffc171 100644 --- a/signup-ui/public/locales/km.json +++ b/signup-ui/public/locales/km.json @@ -52,5 +52,19 @@ "privacy_and_policy_title": "Privacy & Policy", "footer": { "powered_by": "ដំណើរការដោយ" + }, + "error_response": { + "invalid_transaction": "", + "invalid_otp_channel": "", + "invalid_captcha": "", + "send_otp_failed": "", + "active_otp_found": "", + "unknown_error": "", + "challenge_failed": "", + "invalid_challenge_type": "", + "invalid_challenge_format": "", + "already-registered": "", + "timed_out": "", + "request_limit": "" } } diff --git a/signup-ui/src/components/language.tsx b/signup-ui/src/components/language.tsx index 2cd62efa..5215c1b9 100644 --- a/signup-ui/src/components/language.tsx +++ b/signup-ui/src/components/language.tsx @@ -2,11 +2,10 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { useTranslation } from "react-i18next"; import { ReactComponent as TranslationIcon } from "~assets/svg/translation-icon.svg"; -import { langFontMapping } from "~constants/language"; +import { langFontMapping, languages_2Letters } from "~constants/language"; +import { cn } from "~utils/cn"; -import locales from "../../public/locales/default.json"; import { Icons } from "./ui/icons"; -import { cn } from "~utils/cn"; export const Language = () => { const { i18n } = useTranslation(); @@ -18,38 +17,36 @@ export const Language = () => { return (
- + { - locales.languages_2Letters[ - i18n.language as keyof typeof locales.languages_2Letters + languages_2Letters[ + i18n.language as keyof typeof languages_2Letters ] } - + - {Object.entries(locales.languages_2Letters).map(([key, value]) => ( + {Object.entries(languages_2Letters).map(([key, value]) => ( handleLanguageChange(key)} > {value} diff --git a/signup-ui/src/components/ui/active-message.tsx b/signup-ui/src/components/ui/active-message.tsx new file mode 100644 index 00000000..f20d033a --- /dev/null +++ b/signup-ui/src/components/ui/active-message.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; + +import { cn } from "~utils/cn"; + +export interface ActiveMessageProps + extends React.HTMLAttributes {} + +const ActiveMessage = React.forwardRef( + ({ className, hidden, children, ...props }, ref) => ( +
+ {children} +
+ ) +); + +ActiveMessage.displayName = "ActiveMessage"; + +export { ActiveMessage }; diff --git a/signup-ui/src/constants/language.ts b/signup-ui/src/constants/language.ts index 732bbdc7..84312dac 100644 --- a/signup-ui/src/constants/language.ts +++ b/signup-ui/src/constants/language.ts @@ -1,3 +1,15 @@ +export const languages_2Letters = { + km: "ខ្មែរ", + en: "English", +}; + +export const rtlLanguages = []; + +export const langCodeMapping = { + khm: "km", + eng: "en", +}; + export const langFontMapping: { [key: string]: string } = { en: "font-inter", km: "font-kantumruypro", diff --git a/signup-ui/src/pages/SignUpPage/AccountRegistrationStatus/AccountRegistrationStatus.tsx b/signup-ui/src/pages/SignUpPage/AccountRegistrationStatus/AccountRegistrationStatus.tsx index e7bd1193..9a9ac164 100644 --- a/signup-ui/src/pages/SignUpPage/AccountRegistrationStatus/AccountRegistrationStatus.tsx +++ b/signup-ui/src/pages/SignUpPage/AccountRegistrationStatus/AccountRegistrationStatus.tsx @@ -1,33 +1,112 @@ +import { useMutationState, useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; -import { useLocation } from "react-router-dom"; -import { ReactComponent as SuccessIconSvg } from "~assets/svg/success-icon.svg"; -import { Button } from "~components/ui/button"; -import { Step, StepContent } from "~components/ui/step"; -import { getSignInRedirectURL } from "~utils/link"; +import { keys as mutationKeys } from "~pages/SignUpPage/mutations"; +import { keys as queryKeys } from "~pages/SignUpPage/queries"; +import { + RegistrationResponseDto, + RegistrationStatus, + RegistrationStatusResponseDto, + RegistrationWithFailedStatus, +} from "~typings/types"; + +import { AccountRegistrationStatusLayout } from "./components/AccountRegistrationStatusLayout"; export const AccountRegistrationStatus = () => { const { t } = useTranslation(); - const { hash: fromSignInHash } = useLocation(); - const handleAction = () => { - window.location.href = getSignInRedirectURL(fromSignInHash); - }; + const queryClient = useQueryClient(); + + const [registration] = useMutationState({ + filters: { mutationKey: mutationKeys.registration, status: "success" }, + select: (mutation) => mutation.state.data as RegistrationResponseDto, + }); + + const registrationStatus = + queryClient.getQueryData( + queryKeys.registrationStatus + ); + + const registrationStatusState = queryClient.getQueryState( + queryKeys.registrationStatus + ); + + if (!registration) { + return ( + + ); + } + + if (registration && registration.errors.length > 0) { + return ( + + ); + } + + if ( + registration.response?.status === RegistrationStatus.PENDING && + registrationStatusState?.error + ) { + return ( + + ); + } + + if ( + registration.response?.status === RegistrationStatus.PENDING && + !registrationStatus + ) { + return ( + + ); + } + + if ( + registration.response?.status === RegistrationStatus.PENDING && + registrationStatus + ) { + if (registrationStatus.errors.length > 0) { + return ( + + ); + } + if ( + registrationStatus.response && + [ + RegistrationWithFailedStatus.FAILED, + RegistrationWithFailedStatus.PENDING, + ].includes(registrationStatus.response.status) + ) { + return ( + + ); + } + } + return ( - - -
- -
-

{t("congratulations")}

-

{t("account_created_successfully")}

-
-

{t("login_to_proceed")}

-
- -
-
+ ); }; diff --git a/signup-ui/src/pages/SignUpPage/AccountRegistrationStatus/components/AccountRegistrationStatusLayout.tsx b/signup-ui/src/pages/SignUpPage/AccountRegistrationStatus/components/AccountRegistrationStatusLayout.tsx new file mode 100644 index 00000000..8b3f5003 --- /dev/null +++ b/signup-ui/src/pages/SignUpPage/AccountRegistrationStatus/components/AccountRegistrationStatusLayout.tsx @@ -0,0 +1,50 @@ +import { useTranslation } from "react-i18next"; +import { useLocation } from "react-router-dom"; + +import { ReactComponent as FailedIconSvg } from "~assets/svg/failed-icon.svg"; +import { ReactComponent as SuccessIconSvg } from "~assets/svg/success-icon.svg"; +import { Button } from "~components/ui/button"; +import { Step, StepContent } from "~components/ui/step"; +import { getSignInRedirectURL } from "~utils/link"; + +interface AccountRegistrationStatusLayoutProps { + status: "success" | "failed"; + message: string; +} + +export const AccountRegistrationStatusLayout = ({ + status, + message, +}: AccountRegistrationStatusLayoutProps) => { + const { t } = useTranslation(); + const { hash: fromSignInHash } = useLocation(); + + const handleAction = (e: any) => { + e.preventDefault(); + window.location.href = getSignInRedirectURL(fromSignInHash); + }; + + return ( + + +
+ {status === "success" ? : } +
+ {status === "success" ? ( + <> +

{t("congratulations")}

+

{t("account_created_successfully")}

+ + ) : ( +

{t("signup_failed")}

+ )} +
+

{message}

+
+ +
+
+ ); +}; diff --git a/signup-ui/src/pages/SignUpPage/AccountSetup/AccountSetup.tsx b/signup-ui/src/pages/SignUpPage/AccountSetup/AccountSetup.tsx index c0cc408f..7bcd05c2 100644 --- a/signup-ui/src/pages/SignUpPage/AccountSetup/AccountSetup.tsx +++ b/signup-ui/src/pages/SignUpPage/AccountSetup/AccountSetup.tsx @@ -24,15 +24,16 @@ import { StepTitle, } from "~components/ui/step"; import { cn } from "~utils/cn"; -import { - RegistrationRequestDto, - RegistrationStatus, - SettingsDto, -} from "~typings/types"; +import { RegistrationRequestDto, SettingsDto } from "~typings/types"; import { useRegister } from "../mutations"; import { SignUpForm } from "../SignUpPage"; -import { setStepSelector, SignUpStep, useSignUpStore } from "../useSignUpStore"; +import { + setCriticalErrorSelector, + setStepSelector, + SignUpStep, + useSignUpStore, +} from "../useSignUpStore"; import { TermsAndPrivacyModal } from "./components/TermsAndPrivacyModal"; interface AccountSetupProps { @@ -43,13 +44,20 @@ interface AccountSetupProps { export const AccountSetup = ({ settings, methods }: AccountSetupProps) => { const { t } = useTranslation(); - const { setStep } = useSignUpStore( - useCallback((state) => ({ setStep: setStepSelector(state) }), []) + const { setStep, setCriticalError } = useSignUpStore( + useCallback( + (state) => ({ + setStep: setStepSelector(state), + setCriticalError: setCriticalErrorSelector(state), + }), + [] + ) ); const { control, trigger, getValues, + reset, formState: { errors: formErrors, isValid }, } = methods; @@ -68,7 +76,7 @@ export const AccountSetup = ({ settings, methods }: AccountSetupProps) => { request: { username: `${ settings.response.configs["identifier.prefix"] - } ${getValues("phone")}`, + }${getValues("phone")}`, password: getValues("password"), consent: getValues("consent") ? "AGREE" : "DISAGREE", userInfo: { @@ -84,16 +92,14 @@ export const AccountSetup = ({ settings, methods }: AccountSetupProps) => { }; return registerMutation.mutate(RegistrationRequestDto, { - onSuccess: ({ response, errors }) => { - if (!errors) { - if (response.status === RegistrationStatus.PENDING) { - // direct user to status step when the account creation is pending - setStep(SignUpStep.AccountSetupStatus); - } - if (response.status === RegistrationStatus.COMPLETED) { - // direct user to the final (success) step when user successfully create the account - setStep(SignUpStep.AccountRegistrationStatus); - } + onSuccess: ({ errors }) => { + if ( + errors.length > 0 && + errors[0].errorCode === "invalid_transaction" + ) { + setCriticalError(errors[0]); + } else { + setStep(SignUpStep.AccountSetupStatus); } }, }); @@ -257,7 +263,7 @@ export const AccountSetup = ({ settings, methods }: AccountSetupProps) => { @@ -267,17 +273,19 @@ export const AccountSetup = ({ settings, methods }: AccountSetupProps) => { TermsAndConditionsAnchor: ( ), PrivacyPolicyAnchor: ( ), }} @@ -291,6 +299,7 @@ export const AccountSetup = ({ settings, methods }: AccountSetupProps) => { className="w-full" onClick={handleContinue} disabled={!isValid} + isLoading={registerMutation.isPending} > {t("continue")} diff --git a/signup-ui/src/pages/SignUpPage/AccountSetupStatus/AccountSetupStatus.tsx b/signup-ui/src/pages/SignUpPage/AccountSetupStatus/AccountSetupStatus.tsx index a7fa5917..89325d04 100644 --- a/signup-ui/src/pages/SignUpPage/AccountSetupStatus/AccountSetupStatus.tsx +++ b/signup-ui/src/pages/SignUpPage/AccountSetupStatus/AccountSetupStatus.tsx @@ -1,10 +1,17 @@ import { useCallback, useEffect } from "react"; +import { useMutationState } from "@tanstack/react-query"; import { UseFormReturn } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { Icons } from "~components/ui/icons"; import { Step, StepContent } from "~components/ui/step"; -import { RegistrationWithFailedStatus, SettingsDto } from "~typings/types"; +import { keys as mutationKeys } from "~pages/SignUpPage/mutations"; +import { + RegistrationResponseDto, + RegistrationStatus, + RegistrationWithFailedStatus, + SettingsDto, +} from "~typings/types"; import { useRegistrationStatus } from "../queries"; import { SignUpForm } from "../SignUpPage"; @@ -26,29 +33,49 @@ export const AccountSetupStatus = ({ ); const { trigger } = methods; - const { data: registrationStatus, isError } = useRegistrationStatus( - settings.response.configs["status.request.limit"], - settings.response.configs["status.request.delay"] - ); + const [registration] = useMutationState({ + filters: { mutationKey: mutationKeys.registration, status: "success" }, + select: (mutation) => mutation.state.data as RegistrationResponseDto, + }); useEffect(() => { - if (isError) { - // TODO: handle case the request limit is reached - } - + // go to the last step on response status is COMPLETED or on error(s) if ( - registrationStatus?.response.status === - RegistrationWithFailedStatus.FAILED + (registration.response && + registration.response.status === RegistrationStatus.COMPLETED) || + registration.errors.length > 0 ) { - // TODO: handle case the registration status failed + setStep(SignUpStep.AccountRegistrationStatus); } + }, [registration]); + + // isError occurs when the query encounters a network error or the request limit attempts is reached + const { data: registrationStatus, isError: isRegistrationStatusError } = + useRegistrationStatus( + settings.response.configs["status.request.limit"], + settings.response.configs["status.request.delay"], + registration + ); + + useEffect(() => { + // go to the last step on registration status FAILED or COMPLETED or reach the request limit if ( - registrationStatus?.response.status === - RegistrationWithFailedStatus.COMPLETED + (registrationStatus && + (registrationStatus.response?.status === + RegistrationWithFailedStatus.FAILED || + registrationStatus.response?.status === + RegistrationWithFailedStatus.COMPLETED || + registrationStatus.errors.length > 0)) || + isRegistrationStatusError ) { setStep(SignUpStep.AccountRegistrationStatus); } - }, [registrationStatus, settings.response.configs, isError, setStep]); + }, [ + registrationStatus, + setStep, + isRegistrationStatusError, + settings.response.configs, + ]); return ( diff --git a/signup-ui/src/pages/SignUpPage/Otp/Otp.tsx b/signup-ui/src/pages/SignUpPage/Otp/Otp.tsx index b6a7227d..18a92722 100644 --- a/signup-ui/src/pages/SignUpPage/Otp/Otp.tsx +++ b/signup-ui/src/pages/SignUpPage/Otp/Otp.tsx @@ -2,23 +2,15 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useFormContext, UseFormReturn } from "react-hook-form"; import { Trans, useTranslation } from "react-i18next"; import PinInput from "react-pin-input"; -import { useSearchParams } from "react-router-dom"; +import { useTimer } from "react-timer-hook"; -import { ReactComponent as FailedIconSvg } from "~assets/svg/failed-icon.svg"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "~components/ui/alert-dialog"; +import { ActiveMessage } from "~components/ui/active-message"; import { Button } from "~components/ui/button"; import { FormControl, FormField, FormItem } from "~components/ui/form"; import { Icons } from "~components/ui/icons"; import { Step, + StepAlert, StepContent, StepDescription, StepDivider, @@ -26,8 +18,9 @@ import { StepTitle, } from "~components/ui/step"; import { cn } from "~utils/cn"; +import { getLocale } from "~utils/language"; import { maskPhoneNumber } from "~utils/phone"; -import { convertTime } from "~utils/timer"; +import { convertTime, getTimeoutTime } from "~utils/timer"; import { Error, GenerateChallengeRequestDto, @@ -38,13 +31,13 @@ import { import { useGenerateChallenge, useVerifyChallenge } from "../mutations"; import { SignUpForm } from "../SignUpPage"; import { + setCriticalErrorSelector, setStepSelector, SignUpStep, stepSelector, useSignUpStore, } from "../useSignUpStore"; import { ResendAttempt } from "./components/ResendAttempt"; -import { useTimer } from "./hooks/useTimer"; interface OtpProps { settings: SettingsDto; @@ -52,44 +45,47 @@ interface OtpProps { } export const Otp = ({ methods, settings }: OtpProps) => { - const { t } = useTranslation(); + const { i18n, t } = useTranslation(); - const [hasError, setHasError] = useState(false); const pinInputRef = useRef(null); const { control, getValues, setValue } = useFormContext(); - const { step, setStep } = useSignUpStore( + const { step, setStep, setCriticalError } = useSignUpStore( useCallback( (state) => ({ step: stepSelector(state), setStep: setStepSelector(state), + setCriticalError: setCriticalErrorSelector(state), }), [] ) ); const { trigger, reset, formState } = methods; const [resendAttempts, setResendAttempts] = useState(0); - const [enableResendOtp, setEnableResendOtp] = useState(false); - const [timeLeft, setTimeLeft] = useTimer(0, setEnableResendOtp); const { generateChallengeMutation } = useGenerateChallenge(); const { verifyChallengeMutation } = useVerifyChallenge(); - const [error, setError] = useState(null); - const [showDialog, setShowDialog] = useState(false); - const [searchParams] = useSearchParams(); + const [challengeVerificationError, setChallengeVerificationError] = + useState(null); + + const { + totalSeconds: resendOtpTotalSecs, + restart: restartResendOtpTotalSecs, + } = useTimer({ + expiryTimestamp: getTimeoutTime(settings.response.configs["resend.delay"]), + }); useEffect(() => { - setTimeLeft(settings.response.configs["resend.delay"]); setResendAttempts(settings.response.configs["resend.attempts"]); - }, [settings.response.configs, setTimeLeft]); + }, [settings.response.configs]); useEffect(() => { - if (!hasError) return; + if (!challengeVerificationError) return; const intervalId = setInterval(() => { - setHasError(false); - }, 3 * 1000); + setChallengeVerificationError(null); + }, settings.response.configs["popup.timeout"] * 1000); return () => clearInterval(intervalId); - }, [hasError]); + }, [challengeVerificationError]); const handlePinInputRef = (n: PinInput | null) => { pinInputRef.current = n; @@ -107,11 +103,17 @@ export const Otp = ({ methods, settings }: OtpProps) => { (e: any) => { e.preventDefault(); if (settings?.response.configs && resendAttempts > 0) { + setChallengeVerificationError(null); + const generateChallengeRequestDto: GenerateChallengeRequestDto = { requestTime: new Date().toISOString(), request: { - identifier: getValues("phone"), + identifier: `${ + settings.response.configs["identifier.prefix"] + }${getValues("phone")}`, captchaToken: getValues("captchaToken"), + locale: getLocale(i18n.language), + regenerate: true, }, }; @@ -121,11 +123,12 @@ export const Otp = ({ methods, settings }: OtpProps) => { setValue("otp", "", { shouldValidate: true }); setResendAttempts((resendAttempt) => resendAttempt - 1); - setTimeLeft(settings.response.configs["resend.delay"]); - setEnableResendOtp(false); + restartResendOtpTotalSecs( + getTimeoutTime(settings.response.configs["resend.delay"]) + ); - if (errors) { - setError(errors[0]); + if (errors && errors.length > 0) { + setChallengeVerificationError(errors[0]); } }, }); @@ -137,7 +140,7 @@ export const Otp = ({ methods, settings }: OtpProps) => { generateChallengeMutation, getValues, setValue, - setTimeLeft, + restartResendOtpTotalSecs, ] ); @@ -154,10 +157,14 @@ export const Otp = ({ methods, settings }: OtpProps) => { const isStepValid = await trigger(); if (isStepValid) { + setChallengeVerificationError(null); + const verifyChallengeRequestDto: VerifyChallengeRequestDto = { requestTime: new Date().toISOString(), request: { - identifier: getValues("phone"), + identifier: `${ + settings.response.configs["identifier.prefix"] + }${getValues("phone")}`, challengeInfo: { challenge: getValues("otp"), format: "alpha-numeric", @@ -167,16 +174,22 @@ export const Otp = ({ methods, settings }: OtpProps) => { return verifyChallengeMutation.mutate(verifyChallengeRequestDto, { onSuccess: ({ errors }) => { - if (!errors) { - setStep(SignUpStep.PhoneStatus); + if (errors.length > 0) { + if (errors[0].errorCode === "already-registered") { + setStep(SignUpStep.PhoneStatus); + } else if (errors[0].errorCode === "invalid_transaction") { + setCriticalError(errors[0]); + } else { + setChallengeVerificationError(errors[0]); + } } - if (errors) { - setError(errors[0]); + if (errors.length === 0) { + setStep(SignUpStep.PhoneStatus); } }, onError: () => { - setHasError(true); + setChallengeVerificationError(null); }, }); } @@ -184,184 +197,142 @@ export const Otp = ({ methods, settings }: OtpProps) => { [setStep, trigger, getValues, verifyChallengeMutation] ); - const handleErrorRedirect = () => { - const esignetLoginPage = searchParams.get("callback"); - if (error?.errorCode === "already-registered" && esignetLoginPage) { - window.location.href = esignetLoginPage; - } else { - setStep(SignUpStep.Phone); - reset(); - } - }; - const handleExhaustedAttempt = () => { setStep(SignUpStep.Phone); reset(); }; return ( - <> - - - - - - Error! - - - {error?.errorMessage} - - - - - {error?.errorCode === "already-registered" && - searchParams.get("callback") - ? t("login") - : t("okay")} - - - - - - - - -
- {t("otp_header")} -
-
- -
- {t("otp_subheader", { - no_of_digit: settings?.response.configs["otp.length"], - })} -
-
- {settings.response.configs["identifier.prefix"]}{" "} - {maskPhoneNumber(getValues("phone"), 4)} -
-
-
- - - {/* Error message */} -
+ + + +
+ {t("otp_header")} +
+
+ +
+ {t("otp_subheader", { + no_of_digit: settings?.response.configs["otp.length"], + })} +
+
+ {settings.response.configs["identifier.prefix"]}{" "} + {maskPhoneNumber(getValues("phone"), 4)} +
+
+
+ + + {/* Error message */} + + + + {/* OTP inputs */} +
+ ( + + + { + //TO handle case when user pastes OTP + handleOtpComplete(value); + }} + onChange={(value, _) => { + handleOtpChange(value); + }} + /> + + )} + /> +
- {/* OTP inputs */} -
- ( - - - { - //TO handle case when user pastes OTP - handleOtpComplete(value); - }} - onChange={(value, _) => { - handleOtpChange(value); - }} - /> - - - )} - /> + {t("verify")} + +
+
+ , + }} + values={{ countDown: convertTime(resendOtpTotalSecs) }} + /> +
-
-
- , - }} - values={{ countDown: convertTime(timeLeft) }} - /> -
+ {resendAttempts !== + settings.response.configs["resend.attempts"] && ( + + )} + {resendAttempts === 0 && ( - {resendAttempts !== - settings.response.configs["resend.attempts"] && ( - - )} - {resendAttempts === 0 && ( - - )} -
+ )}
- - - +
+
+ ); }; diff --git a/signup-ui/src/pages/SignUpPage/Phone/Phone.tsx b/signup-ui/src/pages/SignUpPage/Phone/Phone.tsx index a5fef2cd..f5f4e274 100644 --- a/signup-ui/src/pages/SignUpPage/Phone/Phone.tsx +++ b/signup-ui/src/pages/SignUpPage/Phone/Phone.tsx @@ -4,13 +4,13 @@ import { useFormContext, UseFormReturn } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useLocation } from "react-router-dom"; +import { ActiveMessage } from "~components/ui/active-message"; import { Button } from "~components/ui/button"; import { FormControl, FormField, FormItem, FormMessage, - useFormField, } from "~components/ui/form"; import { Icons } from "~components/ui/icons"; import { Input } from "~components/ui/input"; @@ -23,6 +23,7 @@ import { StepTitle, } from "~components/ui/step"; import { cn } from "~utils/cn"; +import { getLocale } from "~utils/language"; import { getSignInRedirectURL } from "~utils/link"; import { Error, @@ -32,17 +33,28 @@ import { import { useGenerateChallenge } from "../mutations"; import { SignUpForm } from "../SignUpPage"; -import { setStepSelector, SignUpStep, useSignUpStore } from "../useSignUpStore"; +import { + setCriticalErrorSelector, + setStepSelector, + SignUpStep, + useSignUpStore, +} from "../useSignUpStore"; interface PhoneProps { settings: SettingsDto; methods: UseFormReturn; } export const Phone = ({ settings, methods }: PhoneProps) => { - const { t } = useTranslation(); + const { i18n, t } = useTranslation(); - const { setStep } = useSignUpStore( - useCallback((state) => ({ setStep: setStepSelector(state) }), []) + const { setStep, setCriticalError } = useSignUpStore( + useCallback( + (state) => ({ + setStep: setStepSelector(state), + setCriticalError: setCriticalErrorSelector(state), + }), + [] + ) ); const [hasError, setHasError] = useState(false); const { control, setValue, getValues } = useFormContext(); @@ -61,7 +73,7 @@ export const Phone = ({ settings, methods }: PhoneProps) => { const intervalId = setInterval(() => { setHasError(false); - }, 3 * 1000); + }, settings.response.configs["popup.timeout"] * 1000); return () => clearInterval(intervalId); }, [hasError]); @@ -80,24 +92,34 @@ export const Phone = ({ settings, methods }: PhoneProps) => { const isStepValid = await trigger(); if (isStepValid) { + setHasError(false); + const generateChallengeRequestDto: GenerateChallengeRequestDto = { requestTime: new Date().toISOString(), request: { - identifier: getValues("phone"), + identifier: `${ + settings.response.configs["identifier.prefix"] + }${getValues("phone")}`, captchaToken: getValues("captchaToken"), + locale: getLocale(i18n.language), + regenerate: false, }, }; return generateChallengeMutation.mutate(generateChallengeRequestDto, { - onSuccess: ({ errors }) => { - if (!errors) { - setValue("otp", "", { shouldValidate: true }); - setStep(SignUpStep.Otp); + onSuccess: ({ response, errors }) => { + if (errors.length > 0) { + if (errors[0].errorCode === "invalid_transaction") { + setCriticalError(errors[0]); + } else { + setError(errors[0]); + setHasError(true); + } } - if (errors) { - setError(errors[0]); - setHasError(true); + if (response && errors.length === 0) { + setValue("otp", "", { shouldValidate: true }); + setStep(SignUpStep.Otp); } }, onError: () => { @@ -129,20 +151,17 @@ export const Phone = ({ settings, methods }: PhoneProps) => { {/* Error message */} -
-

{error?.errorMessage}

+
+
{/* Phone and reCAPTCHA inputs */} @@ -188,7 +207,7 @@ export const Phone = ({ settings, methods }: PhoneProps) => { onChange={handleReCaptchaChange} onExpired={handleReCaptchaExpired} className="recaptcha" - sitekey={process.env.REACT_APP_CAPTCHA_SITE_KEY ?? ""} + sitekey={settings.response.configs["captcha.site.key"] ?? ""} />
diff --git a/signup-ui/src/pages/SignUpPage/PhoneStatus/PhoneStatus.tsx b/signup-ui/src/pages/SignUpPage/PhoneStatus/PhoneStatus.tsx index fcf03936..3fc4cf23 100644 --- a/signup-ui/src/pages/SignUpPage/PhoneStatus/PhoneStatus.tsx +++ b/signup-ui/src/pages/SignUpPage/PhoneStatus/PhoneStatus.tsx @@ -1,10 +1,16 @@ import { useCallback } from "react"; +import { useMutationState } from "@tanstack/react-query"; import { UseFormReturn } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { useLocation } from "react-router-dom"; +import { ReactComponent as FailedIconSvg } from "~assets/svg/failed-icon.svg"; import { ReactComponent as SuccessIconSvg } from "~assets/svg/success-icon.svg"; import { Button } from "~components/ui/button"; import { Step, StepContent } from "~components/ui/step"; +import { getSignInRedirectURL } from "~utils/link"; +import { keys as mutationKeys } from "~pages/SignUpPage/mutations"; +import { VerifyChallengeResponseDto } from "~typings/types"; import { SignUpForm } from "../SignUpPage"; import { setStepSelector, SignUpStep, useSignUpStore } from "../useSignUpStore"; @@ -19,6 +25,7 @@ export const PhoneStatus = ({ methods }: PhoneStatusProps) => { const { setStep } = useSignUpStore( useCallback((state) => ({ setStep: setStepSelector(state) }), []) ); + const { hash: fromSignInHash } = useLocation(); const { trigger } = methods; const handleContinue = useCallback( @@ -33,6 +40,46 @@ export const PhoneStatus = ({ methods }: PhoneStatusProps) => { [trigger, setStep] ); + const handleChallengeVerificationErrorRedirect = (e: any) => { + e.preventDefault(); + window.location.href = getSignInRedirectURL(fromSignInHash); + }; + + const [challengeVerification] = useMutationState({ + filters: { + mutationKey: mutationKeys.challengeVerification, + status: "success", + }, + select: (mutation) => mutation.state.data as VerifyChallengeResponseDto, + }); + + if ( + challengeVerification.errors.length > 0 && + challengeVerification.errors[0].errorCode === "already-registered" + ) { + return ( + + +
+ +

+ {t("signup_failed")} +

+

+ {t("mobile_number_already_registered")} +

+
+ +
+
+ ); + } + return ( diff --git a/signup-ui/src/pages/SignUpPage/SignUpPage.tsx b/signup-ui/src/pages/SignUpPage/SignUpPage.tsx index f8618c1c..65834c19 100644 --- a/signup-ui/src/pages/SignUpPage/SignUpPage.tsx +++ b/signup-ui/src/pages/SignUpPage/SignUpPage.tsx @@ -1,6 +1,6 @@ import { useCallback, useMemo } from "react"; import { yupResolver } from "@hookform/resolvers/yup"; -import { isValidPhoneNumber } from "libphonenumber-js"; +import { AsYouType, isValidPhoneNumber } from "libphonenumber-js"; import { Resolver, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import * as yup from "yup"; @@ -15,7 +15,13 @@ import AccountSetupStatus from "./AccountSetupStatus"; import Otp from "./Otp"; import Phone from "./Phone"; import PhoneStatus from "./PhoneStatus"; -import { SignUpStep, stepSelector, useSignUpStore } from "./useSignUpStore"; +import { SignUpPopover } from "./SignUpPopover"; +import { + criticalErrorSelector, + SignUpStep, + stepSelector, + useSignUpStore, +} from "./useSignUpStore"; export interface SignUpForm { phone: string; @@ -35,8 +41,14 @@ interface SignUpPageProps { export const SignUpPage = ({ settings }: SignUpPageProps) => { const { t } = useTranslation(); - const { step } = useSignUpStore( - useCallback((state) => ({ step: stepSelector(state) }), []) + const { step, criticalError } = useSignUpStore( + useCallback( + (state) => ({ + step: stepSelector(state), + criticalError: criticalErrorSelector(state), + }), + [] + ) ); const validationSchema = useMemo(() => { @@ -47,9 +59,12 @@ export const SignUpPage = ({ settings }: SignUpPageProps) => { .string() .required() .min(1, t("fail_to_send_otp")) - .test("is-phone-number", t("fail_to_send_otp"), (phone) => - isValidPhoneNumber(phone, "KH") - ), + .test("is-phone-number", t("fail_to_send_otp"), (phone) => { + const asYouType = new AsYouType(); + asYouType.input(settings.response.configs["identifier.prefix"]); + + return isValidPhoneNumber(phone, asYouType.country); + }), captchaToken: yup.string().required(t("captcha_token_validation")), }), // Step 2 - OTP Validation @@ -84,6 +99,7 @@ export const SignUpPage = ({ settings }: SignUpPageProps) => { }), // Step 5 - Register Status Validation yup.object({}), + yup.object({}), ]; }, [settings, t]); @@ -130,8 +146,14 @@ export const SignUpPage = ({ settings }: SignUpPageProps) => { }; return ( -
- {getSignUpStepContent(step)}
- + <> + {criticalError && + ["invalid_transaction"].includes(criticalError.errorCode) && ( + + )} +
+ {getSignUpStepContent(step)}
+ + ); }; diff --git a/signup-ui/src/pages/SignUpPage/SignUpPopover.tsx b/signup-ui/src/pages/SignUpPage/SignUpPopover.tsx new file mode 100644 index 00000000..bc95944d --- /dev/null +++ b/signup-ui/src/pages/SignUpPage/SignUpPopover.tsx @@ -0,0 +1,62 @@ +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "react-router-dom"; + +import { ReactComponent as FailedIconSvg } from "~assets/svg/failed-icon.svg"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "~components/ui/alert-dialog"; +import { getSignInRedirectURL } from "~utils/link"; + +import { criticalErrorSelector, useSignUpStore } from "./useSignUpStore"; + +export const SignUpPopover = () => { + const { t } = useTranslation(); + + const { criticalError } = useSignUpStore( + useCallback( + (state) => ({ + criticalError: criticalErrorSelector(state), + }), + [] + ) + ); + const { hash: fromSignInHash } = useLocation(); + + const handleAction = (e: any) => { + e.preventDefault(); + window.location.href = getSignInRedirectURL(fromSignInHash); + }; + + return ( + + + + + <> + + {t("error")} + + + + {criticalError && t(`error_response.${criticalError.errorCode}`)} + + + + + {t("okay")} + + + + + ); +}; diff --git a/signup-ui/src/pages/SignUpPage/mutations.ts b/signup-ui/src/pages/SignUpPage/mutations.ts index b945e5d6..971468d1 100644 --- a/signup-ui/src/pages/SignUpPage/mutations.ts +++ b/signup-ui/src/pages/SignUpPage/mutations.ts @@ -12,12 +12,19 @@ import { import { generateChallenge, register, verifyChallenge } from "./service"; +export const keys = { + challengeGeneration: ["challengeGeneration"] as const, + challengeVerification: ["challengeVerification"] as const, + registration: ["registration"] as const, +}; + export const useGenerateChallenge = () => { const generateChallengeMutation = useMutation< GenerateChallengeResponseDto, ApiError, GenerateChallengeRequestDto >({ + mutationKey: keys.challengeGeneration, mutationFn: (generateChallengeRequestDto: GenerateChallengeRequestDto) => generateChallenge(generateChallengeRequestDto), }); @@ -31,6 +38,7 @@ export const useVerifyChallenge = () => { ApiError, VerifyChallengeRequestDto >({ + mutationKey: keys.challengeVerification, mutationFn: (verifyChallengeRequestDto: VerifyChallengeRequestDto) => verifyChallenge(verifyChallengeRequestDto), }); @@ -44,8 +52,10 @@ export const useRegister = () => { ApiError, RegistrationRequestDto >({ + mutationKey: keys.registration, mutationFn: (RegistrationRequestDto: RegistrationRequestDto) => register(RegistrationRequestDto), + gcTime: Infinity, }); return { registerMutation }; diff --git a/signup-ui/src/pages/SignUpPage/queries.ts b/signup-ui/src/pages/SignUpPage/queries.ts index c71d3859..75385c04 100644 --- a/signup-ui/src/pages/SignUpPage/queries.ts +++ b/signup-ui/src/pages/SignUpPage/queries.ts @@ -1,6 +1,11 @@ import { useQuery, UseQueryResult } from "@tanstack/react-query"; -import { RegistrationStatusResponseDto, SettingsDto } from "~typings/types"; +import { + RegistrationResponseDto, + RegistrationStatus, + RegistrationStatusResponseDto, + SettingsDto, +} from "~typings/types"; import { getRegistrationStatus, getSettings } from "./service"; @@ -19,12 +24,17 @@ export const useSettings = (): UseQueryResult => { export const useRegistrationStatus = ( statusRequestAttempt: number, - statusRequestDelay: number + statusRequestDelay: number, + registration: RegistrationResponseDto ): UseQueryResult => { return useQuery({ queryKey: keys.registrationStatus, queryFn: () => getRegistrationStatus(), - retry: statusRequestAttempt, + gcTime: Infinity, + retry: statusRequestAttempt - 1, // minus 1 for we called it once already retryDelay: statusRequestDelay * 1000, + enabled: + !!registration.response && + registration.response.status === RegistrationStatus.PENDING, }); }; diff --git a/signup-ui/src/pages/SignUpPage/service.ts b/signup-ui/src/pages/SignUpPage/service.ts index c2a3977e..079200fd 100644 --- a/signup-ui/src/pages/SignUpPage/service.ts +++ b/signup-ui/src/pages/SignUpPage/service.ts @@ -42,7 +42,7 @@ export const getRegistrationStatus = "/registration/status" ).then(({ data }) => { // treat PENDING as an error so that react-query will auto retry - if (data.response.status === RegistrationWithFailedStatus.PENDING) { + if (data.response?.status === RegistrationWithFailedStatus.PENDING) { throw new Error("Status pending"); } diff --git a/signup-ui/src/pages/SignUpPage/useSignUpStore.tsx b/signup-ui/src/pages/SignUpPage/useSignUpStore.tsx index 88bb4647..58ef3e55 100644 --- a/signup-ui/src/pages/SignUpPage/useSignUpStore.tsx +++ b/signup-ui/src/pages/SignUpPage/useSignUpStore.tsx @@ -2,6 +2,8 @@ import { isEqual } from "lodash"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; +import { Error } from "~typings/types"; + export enum SignUpStep { Phone, Otp, @@ -14,6 +16,8 @@ export enum SignUpStep { export type SignUpStore = { step: SignUpStep; setStep: (step: SignUpStep) => void; + criticalError: Error | null; + setCriticalError: (criticalError: Error | null) => void; }; export const useSignUpStore = create()( @@ -24,6 +28,12 @@ export const useSignUpStore = create()( if (isEqual(current.step, step)) return; set((state) => ({ step })); }, + criticalError: null, + setCriticalError: (criticalError: Error | null) => { + const current = get(); + if (isEqual(current.criticalError, criticalError)) return; + set((state) => ({ criticalError })); + }, })) ); @@ -32,3 +42,11 @@ export const stepSelector = (state: SignUpStore): SignUpStore["step"] => export const setStepSelector = (state: SignUpStore): SignUpStore["setStep"] => state.setStep; + +export const criticalErrorSelector = ( + state: SignUpStore +): SignUpStore["criticalError"] => state.criticalError; + +export const setCriticalErrorSelector = ( + state: SignUpStore +): SignUpStore["setCriticalError"] => state.setCriticalError; diff --git a/signup-ui/src/resources.d.ts b/signup-ui/src/resources.d.ts index 117205b0..f3b067fd 100644 --- a/signup-ui/src/resources.d.ts +++ b/signup-ui/src/resources.d.ts @@ -54,6 +54,20 @@ interface Resources { footer: { powered_by: string; }; + error_response: { + invalid_transaction: string; + invalid_otp_channel: string; + invalid_captcha: string; + send_otp_failed: string; + active_otp_found: string; + unknown_error: string; + challenge_failed: string; + invalid_challenge_type: string; + invalid_challenge_format: string; + "already-registered": string; + timed_out: string; + request_limit: string; + }; }; } diff --git a/signup-ui/src/services/i18n.service.ts b/signup-ui/src/services/i18n.service.ts index 6c9e736e..8374aded 100644 --- a/signup-ui/src/services/i18n.service.ts +++ b/signup-ui/src/services/i18n.service.ts @@ -4,6 +4,8 @@ import Backend from "i18next-http-backend"; import ICU from "i18next-icu"; import { initReactI18next } from "react-i18next"; +import { languages_2Letters } from "~constants/language"; + i18n // follow ICU format .use(ICU) @@ -20,6 +22,7 @@ i18n interpolation: { escapeValue: false, // not needed for react as it escapes by default }, + supportedLngs: Object.keys(languages_2Letters), backend: { loadPath: process.env.PUBLIC_URL + "/locales/{{lng}}.json", }, diff --git a/signup-ui/src/services/langConfig.service.tsx b/signup-ui/src/services/langConfig.service.tsx deleted file mode 100644 index 16148508..00000000 --- a/signup-ui/src/services/langConfig.service.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import axios from "axios"; -import ILangConfig from "../models/langConfig.model"; -import { StringMap } from "../constants/types"; - -const defaultConfigEndpoint = "/locales/default.json"; - -/** - * fetchs and return the locale configuration stored in public folder - * @returns json object - */ -const getLocaleConfiguration = async (): Promise => { - const endpoint = process.env.PUBLIC_URL + defaultConfigEndpoint; - - const response = await axios.get(endpoint); - return response.data; -}; - -const getLangCodeMapping = async (): Promise => { - let localConfig: ILangConfig = await getLocaleConfiguration(); - let reverseMap = Object.entries(localConfig.langCodeMapping).reduce( - (pv, [key, value]) => ((pv[value] = key), pv), - {} as StringMap - ); - return reverseMap; -}; - -const langConfigService = { - getLocaleConfiguration, - getLangCodeMapping, -}; - -export default langConfigService; diff --git a/signup-ui/src/typings/types.ts b/signup-ui/src/typings/types.ts index ac9b4f2a..cdbf4ac8 100644 --- a/signup-ui/src/typings/types.ts +++ b/signup-ui/src/typings/types.ts @@ -49,7 +49,7 @@ export interface Error { export interface BaseResponseDto { responseTime: string; - errors: Error[] | null; + errors: Error[]; } export interface BaseRequestDto { @@ -71,27 +71,31 @@ interface SettingsConfig { "status.check.limit": number; "status.request.limit": number; "status.request.delay": number; + "popup.timeout": number; } export interface Settings { configs: SettingsConfig; } -export interface SettingsDto extends BaseResponseDto { +export type SettingsDto = BaseResponseDto & { response: Settings; -} + errors: Error[] | null; +}; export type GenerateChallengeRequestDto = BaseRequestDto & { request: { identifier: string; captchaToken: string; + locale: string; + regenerate: boolean; }; }; export type GenerateChallengeResponseDto = BaseResponseDto & { response: { status: string; - }; + } | null; }; export type VerifyChallengeRequestDto = BaseRequestDto & { @@ -107,7 +111,7 @@ export type VerifyChallengeRequestDto = BaseRequestDto & { export type VerifyChallengeResponseDto = BaseResponseDto & { response: { status: string; - }; + } | null; }; export interface LanguageTaggedValue { @@ -138,7 +142,7 @@ export enum RegistrationStatus { export type RegistrationResponseDto = BaseResponseDto & { response: { status: RegistrationStatus; - }; + } | null; }; export enum RegistrationWithFailedStatus { @@ -150,5 +154,5 @@ export enum RegistrationWithFailedStatus { export type RegistrationStatusResponseDto = BaseResponseDto & { response: { status: RegistrationWithFailedStatus; - }; + } | null; }; diff --git a/signup-ui/src/utils/language.ts b/signup-ui/src/utils/language.ts new file mode 100644 index 00000000..6b8b8870 --- /dev/null +++ b/signup-ui/src/utils/language.ts @@ -0,0 +1,10 @@ +import { langCodeMapping } from "~constants/language"; + +export const getLocale = (currentLang: string) => { + return ( + Object.keys(langCodeMapping).find( + (key) => + langCodeMapping[key as keyof typeof langCodeMapping] === currentLang + ) ?? Object.keys(langCodeMapping)[0] + ); +}; diff --git a/signup-ui/src/utils/timer.ts b/signup-ui/src/utils/timer.ts index b786e18e..6755502e 100644 --- a/signup-ui/src/utils/timer.ts +++ b/signup-ui/src/utils/timer.ts @@ -13,3 +13,15 @@ export const convertTime = (secondsLeft: number): string => { return `${minutes}:${seconds}`; }; + +/** + * + * @param timeoutInSeconds the number of seconds that will be timeout + * @returns Date object of the timeout + */ +export const getTimeoutTime = (timeoutInSeconds: number): Date => { + const time = new Date(); + time.setSeconds(time.getSeconds() + timeoutInSeconds); // timeout seconds later + + return time; +};