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 */}
+
+
+ {challengeVerificationError &&
+ t(`error_response.${challengeVerificationError.errorCode}`)}
+
+ setChallengeVerificationError(null)}
+ />
+
+
+
+ {/* 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}
+
+
+ {error && t(`error_response.${error.errorCode}`)}
+
setHasError(false)}
+ onClick={() => {
+ setHasError(false);
+ }}
/>
-
+
{/* 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 (
-
-
+ <>
+ {criticalError &&
+ ["invalid_transaction"].includes(criticalError.errorCode) && (
+
+ )}
+
+
+ >
);
};
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;
+};