Skip to content

Commit

Permalink
ES-559 - UI Forgot Password Landing Page (mosip#49)
Browse files Browse the repository at this point in the history
* feat: implement forgot password landing page

* fix: remove popover for fullname

* fix: fix fullname in specific lng validation

* fix: fix phone number validation

* fix: fix username label

* fix: fix generate challenge payload

* fix: add continue loading state

---------

Co-authored-by: Bunsy <[email protected]>
Signed-off-by: Sreang Rathanak <[email protected]>
  • Loading branch information
2 people authored and Sreang Rathanak committed Jan 15, 2024
1 parent 26eda5e commit 8c10d23
Show file tree
Hide file tree
Showing 41 changed files with 760 additions and 90 deletions.
4 changes: 4 additions & 0 deletions signup-ui/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@
"login": "Login",
"okay": "Okay",
"continue": "Continue",
"forgot_password": "Forgot Password?",
"forgot_password_description": "Please enter your 8-9 digit registered mobile number and full name.",
"page_under_construction": "Page Under Construction!",
"page_under_construction_detail": "Our experts are working hard to make this page available. Meanwhile, we request you to please visit after some time.",
"something_went_wrong": "Something went wrong!",
"something_went_wrong_detail": "Our experts are working hard to make things working again.",
"attempts_left": "{attemptLeft} of {totalAttempt} attempts left",
"captcha_token_validation": "Please verify that you are a human.",
"username_validation": "Please enter a valid username",
"full_name_validation": "Please enter a valid name",
"full_name_in_lng_validation": "Full Name has to be in Khmer only.",
"password_validation": "Please enter a valid password",
"password_validation_must_match": "Passwords must match",
"terms_and_conditions_validation": "You must accept the terms and conditions",
Expand Down
4 changes: 4 additions & 0 deletions signup-ui/public/locales/km.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@
"login": "ចូលគណនី",
"okay": "យល់ព្រម",
"continue": "បន្ត",
"forgot_password": "ភ្លេចលេខសម្ងាត់?",
"forgot_password_description": "សូមបញ្ចូលលេខទូរសព្ទដែលបានចុះឈ្មោះ 8-9 ខ្ទង់ និងឈ្មោះពេញរបស់អ្នក",
"page_under_construction": "ទំព័រកំពុងសាងសង់!",
"page_under_construction_detail": "អ្នកជំនាញកំពុងធ្វើការដើម្បីឱ្យទំព័រនេះអាចប្រើប្រាស់បាន។ សូមចូលម្ដងទៀតនៅពេលក្រោយ។",
"something_went_wrong": "មានអ្វីមួយខុសប្រក្រតី!",
"something_went_wrong_detail": "អ្នកជំនាញកំពុងធ្វើការដើម្បីឱ្យអ្វីៗដំណើរការឡើងវិញ។",
"attempts_left": "ការព្យាយាមនៅសល់ {attemptLeft} នៃ {totalAttempt}",
"captcha_token_validation": "សូមបញ្ជាក់ថាអ្នកជាមនុស្ស",
"username_validation": "សូមបញ្ចូលឈ្មោះអ្នកប្រើប្រាស់ត្រឹមត្រូវ",
"full_name_validation": "សូមបញ្ចូលគោត្តនាម-នាមឱ្យបានត្រឹមត្រូវ",
"full_name_in_lng_validation": "គោត្តនាម-នាមត្រូវតែមានតែអក្សរខ្មែរ",
"password_validation": "សូមបញ្ចូលលេខសម្ងាត់ឱ្យបានត្រឹមត្រូវ",
"password_validation_must_match": "លេខចម្ងាត់ត្រូវតែដូចគ្នា",
"terms_and_conditions_validation": "អ្នកត្រូវយល់ព្រមតាមលក្ខខណ្ឌ និងគោលការណ៍",
Expand Down
2 changes: 1 addition & 1 deletion signup-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import Footer from "~components/ui/footer";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
staleTime: 60 * 1000, // set to one minutes
retry: (failureCount, error) => {
// Do not retry on 4xx error codes
if (error instanceof HttpError && String(error.code).startsWith("4")) {
Expand Down
61 changes: 36 additions & 25 deletions signup-ui/src/app/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { lazy, ReactNode, Suspense, useEffect } from "react";
import { Route, Routes, useNavigate, useLocation, Navigate } from "react-router-dom";
import { AppLayout } from "~layouts/AppLayout";
import {
Navigate,
Route,
Routes,
useLocation,
useNavigate,
} from "react-router-dom";

import { ROOT_ROUTE, SIGNUP_ROUTE, SOMETHING_WENT_WRONG, UNDER_CONSTRUCTION } from "~constants/routes";
import Footer from "~components/ui/footer";
import NavBar from "~components/ui/nav-bar";
import {
RESET_PASSWORD,
ROOT_ROUTE,
SIGNUP_ROUTE,
SOMETHING_WENT_WRONG,
UNDER_CONSTRUCTION,
} from "~constants/routes";
import { lazyRetry } from "~utils/lazyRetry";

const SignUpPage = lazy(() => lazyRetry(() => import("~pages/SignUpPage")));
const ResetPasswordPage = lazy(() =>
lazyRetry(() => import("~pages/ResetPasswordPage"))
);
const LandingPage = lazy(() => lazyRetry(() => import("~pages/LandingPage")));
const UnderConstructionPage = lazy(() =>
lazyRetry(() => import("~pages/UnderConstructionPage"))
Expand All @@ -31,26 +45,23 @@ export const AppRouter = () => {
}, [navigate]);

return (
<div className="min-h-screen flex flex-col sm:bg-white">
<NavBar />
<div className="relative flex flex-grow flex-col">
<WithSuspense>
<Routes>
<Route path={SIGNUP_ROUTE} element={<SignUpPage />} />
<Route
path={SOMETHING_WENT_WRONG}
element={<SomethingWentWrongPage />}
/>
<Route
path={UNDER_CONSTRUCTION}
element={<UnderConstructionPage />}
/>
<Route path={ROOT_ROUTE} element={<LandingPage />} />
<Route path="*" element={<Navigate to={REDIRECT_ROUTE} />} />
</Routes>
</WithSuspense>
<Footer />
</div>
</div>
<WithSuspense>
<Routes>
<Route element={<AppLayout />}>
<Route path={SIGNUP_ROUTE} element={<SignUpPage />} />
<Route path={RESET_PASSWORD} element={<ResetPasswordPage />} />
<Route
path={SOMETHING_WENT_WRONG}
element={<SomethingWentWrongPage />}
/>
<Route
path={UNDER_CONSTRUCTION}
element={<UnderConstructionPage />}
/>
<Route path={ROOT_ROUTE} element={<LandingPage />} />
<Route path="*" element={<Navigate to={REDIRECT_ROUTE} />} />
</Route>
</Routes>
</WithSuspense>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import * as React from "react";

import { cn } from "~utils/cn";

export interface ActiveMessageProps
export interface ActionMessageProps
extends React.HTMLAttributes<HTMLDivElement> {}

const ActiveMessage = React.forwardRef<HTMLDivElement, ActiveMessageProps>(
const ActionMessage = React.forwardRef<HTMLDivElement, ActionMessageProps>(
({ className, hidden, children, ...props }, ref) => (
<div
ref={ref}
Expand All @@ -22,6 +22,6 @@ const ActiveMessage = React.forwardRef<HTMLDivElement, ActiveMessageProps>(
)
);

ActiveMessage.displayName = "ActiveMessage";
ActionMessage.displayName = "ActionMessage";

export { ActiveMessage };
export { ActionMessage };
2 changes: 1 addition & 1 deletion signup-ui/src/components/ui/nav-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Language } from "~components/language";

const NavBar = () => {
return (
<nav className="sticky top-0 z-40 h-[70px] w-full border-gray-500 bg-white px-2 sm:px-0 py-2 shadow-md">
<nav className="sticky top-0 z-40 h-[70px] w-full border-gray-500 bg-white px-2 py-2 shadow-md sm:px-0">
<div className="container flex h-full items-center justify-between">
<div className="ltr:ml-1 rtl:mr-1 ltr:sm:ml-8 rtl:sm:mr-8">
<span className="text-2xl font-bold tracking-normal text-primary">
Expand Down
2 changes: 1 addition & 1 deletion signup-ui/src/components/ui/step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Step = React.forwardRef<
<div
ref={ref}
className={cn(
"container max-w-lg sm:max-w-none rounded-2xl bg-white px-0 shadow-lg sm:rounded-none sm:shadow-none",
"container max-w-lg rounded-2xl bg-white px-0 shadow-lg sm:max-w-none sm:rounded-none sm:shadow-none",
className
)}
{...props}
Expand Down
16 changes: 16 additions & 0 deletions signup-ui/src/layouts/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Outlet } from "react-router-dom";

import Footer from "~components/ui/footer";
import NavBar from "~components/ui/nav-bar";

export const AppLayout = () => {
return (
<div className="flex min-h-screen flex-col">
<NavBar />
<div className="relative flex flex-grow flex-col">
<Outlet />
<Footer />
</div>
</div>
);
};
21 changes: 21 additions & 0 deletions signup-ui/src/layouts/PageLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
interface PageLayoutProps {
children: React.ReactNode;
}

export const PageLayout = ({ children }: PageLayoutProps) => {
return (
<div className="relative flex flex-1 items-center justify-center">
<img
className="absolute left-1 top-1"
src="/images/top.png"
alt="top left background"
/>
<div className="z-10 w-full">{children}</div>
<img
className="absolute bottom-1 right-1"
src="/images/bottom.png"
alt="bottom right background"
/>
</div>
);
};
3 changes: 3 additions & 0 deletions signup-ui/src/pages/ResetPasswordPage/Otp/Otp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const Otp = () => {
return <div>Otp</div>;
};
1 change: 1 addition & 0 deletions signup-ui/src/pages/ResetPasswordPage/Otp/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Otp as default } from "./Otp";
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ResetPassword = () => {
return <div>ResetPassword</div>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ResetPassword as default } from "./ResetPassword";
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ResetPasswordConfirmation = () => {
return <div>ResetPasswordConfirmation</div>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ResetPasswordConfirmation as default } from "./ResetPasswordConfirmation";
143 changes: 143 additions & 0 deletions signup-ui/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { useCallback, useMemo } from "react";
import { yupResolver } from "@hookform/resolvers/yup";
import { Resolver, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import * as yup from "yup";

import { Form } from "~components/ui/form";
import { ResetPasswordForm, SettingsDto } from "~typings/types";

import Otp from "./Otp";
import ResetPassword from "./ResetPassword";
import ResetPasswordConfirmation from "./ResetPasswordConfirmation";
import { ResetPasswordPopover } from "./ResetPasswordPopover";
import ResetPasswordStatus from "./ResetPasswordStatus";
import {
criticalErrorSelector,
ResetPasswordStep,
stepSelector,
useResetPasswordStore,
} from "./useResetPasswordStore";
import UserInfo from "./UserInfo";

interface ResetPasswordPageProps {
settings: SettingsDto;
}

export const ResetPasswordPage = ({ settings }: ResetPasswordPageProps) => {
const { t } = useTranslation();

const { step, criticalError } = useResetPasswordStore(
useCallback(
(state) => ({
step: stepSelector(state),
criticalError: criticalErrorSelector(state),
}),
[]
)
);

const validationSchema = useMemo(
() => [
// Step 1 - UserInfo
yup.object({
username: yup
.string()
.required(t("username_validation"))
.matches(/^[^0].*$/, t("username_validation"))
.matches(
new RegExp(settings.response.configs["identifier.pattern"]),
t("username_validation")
),
fullname: yup
.string()
.required(t("full_name_validation"))
.matches(
new RegExp(settings.response.configs["fullname.pattern"]),
t("full_name_in_lng_validation")
),
captchaToken: yup.string().required(t("captcha_token_validation")),
}),
// Step 2 - Otp
yup.object({
otp: yup
.string()
.matches(
new RegExp(`^\\d{${settings.response.configs["otp.length"]}}$`)
),
}),
// Step 3 - ResetPassword
yup.object({
newPassword: yup
.string()
.required(t("password_validation"))
.matches(
new RegExp(settings.response.configs["password.pattern"]),
t("password_validation")
),
confirmNewPassword: yup
.string()
.matches(
new RegExp(settings.response.configs["password.pattern"]),
t("password_validation")
)
.oneOf([yup.ref("password")], t("password_validation_must_match")),
}),
// Step 4 - ResetPasswordStatus
yup.object({}),
// Step 5 - ResetPasswordConfirmation
yup.object({}),
],
[settings, t]
);

const currentValidationSchema = validationSchema[step];

const resetPasswordFormDefaultValues: ResetPasswordForm = {
username: "",
fullname: "",
captchaToken: "",
otp: "",
newPassword: "",
confirmNewPassword: "",
};

const methods = useForm<ResetPasswordForm>({
shouldUnregister: false,
defaultValues: resetPasswordFormDefaultValues,
resolver: yupResolver(currentValidationSchema) as unknown as Resolver<
ResetPasswordForm,
any
>,
mode: "onBlur",
});

const getResetPasswordContent = (step: ResetPasswordStep) => {
switch (step) {
case ResetPasswordStep.UserInfo:
return <UserInfo methods={methods} settings={settings} />;
case ResetPasswordStep.Otp:
return <Otp />;
case ResetPasswordStep.ResetPassword:
return <ResetPassword />;
case ResetPasswordStep.ResetPasswordStatus:
return <ResetPasswordStatus />;
case ResetPasswordStep.ResetPasswordConfirmation:
return <ResetPasswordConfirmation />;
default:
return "unknown step";
}
};

return (
<>
{criticalError &&
["invalid_transaction"].includes(criticalError.errorCode) && (
<ResetPasswordPopover />
)}
<Form {...methods}>
<form>{getResetPasswordContent(step)}</form>
</Form>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { PageLayout } from "~layouts/PageLayout";

import { Icons } from "~components/ui/icons";
import { Step, StepContent } from "~components/ui/step";
import { useSettings } from "~pages/shared/queries";

import { ResetPasswordPage } from "./ResetPasswordPage";

export const ResetPasswordPageContainer = () => {
const { data: settings, isLoading } = useSettings();

if (isLoading || !settings) {
return (
<PageLayout>
<Step>
<StepContent className="flex h-40 items-center justify-center">
<Icons.loader className="animate-spin text-primary" />
</StepContent>
</Step>
</PageLayout>
);
}

return (
<PageLayout>
<ResetPasswordPage settings={settings} />
</PageLayout>
);
};
Loading

0 comments on commit 8c10d23

Please sign in to comment.