From fc912c886f648ee34adafc6c2b759e22e5e2169d Mon Sep 17 00:00:00 2001 From: Emily Jablonski <65367387+emilyjablonski@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:45:16 -0600 Subject: [PATCH] feat: passwordless frontend (#3941) --- shared-helpers/index.ts | 2 + shared-helpers/src/locales/es.json | 4 +- shared-helpers/src/locales/general.json | 22 +++- shared-helpers/src/locales/vi.json | 5 +- shared-helpers/src/locales/zh.json | 4 +- .../src/views/sign-in/FormSignIn.module.scss | 26 ++++ .../src/views/sign-in/FormSignIn.tsx | 72 +++-------- .../src/views/sign-in/FormSignInDefault.tsx | 76 +++++++++++ .../src/views/sign-in/FormSignInPwdless.tsx | 91 ++++++++++++++ sites/partners/src/pages/sign-in.tsx | 27 ++-- sites/public/.env.template | 3 +- sites/public/next.config.js | 1 + sites/public/src/pages/sign-in.tsx | 19 ++- sites/public/src/pages/verify.tsx | 119 ++++++++++++++++++ sites/public/styles/verify.module.scss | 33 +++++ 15 files changed, 428 insertions(+), 76 deletions(-) create mode 100644 shared-helpers/src/views/sign-in/FormSignInDefault.tsx create mode 100644 shared-helpers/src/views/sign-in/FormSignInPwdless.tsx create mode 100644 sites/public/src/pages/verify.tsx create mode 100644 sites/public/styles/verify.module.scss diff --git a/shared-helpers/index.ts b/shared-helpers/index.ts index 50f4aa02d6..d111acb81d 100644 --- a/shared-helpers/index.ts +++ b/shared-helpers/index.ts @@ -26,5 +26,7 @@ export * from "./src/views/summaryTables" export * from "./src/views/forgot-password/FormForgotPassword" export * from "./src/views/layout/ExygyFooter" export * from "./src/views/sign-in/FormSignIn" +export * from "./src/views/sign-in/FormSignInDefault" export * from "./src/views/sign-in/FormSignInErrorBox" +export * from "./src/views/sign-in/FormSignInPwdless" export * from "./src/views/sign-in/ResendConfirmationModal" diff --git a/shared-helpers/src/locales/es.json b/shared-helpers/src/locales/es.json index 18ba7829c8..e8eb483555 100644 --- a/shared-helpers/src/locales/es.json +++ b/shared-helpers/src/locales/es.json @@ -386,8 +386,8 @@ "authentication.signIn.changeYourPassword": "Puede cambiar su contraseña", "authentication.signIn.enterLoginEmail": "Por favor, escriba su correo electrónico de inicio de sesión", "authentication.signIn.enterLoginPassword": "Por favor, escriba su contraseña de inicio de sesión", - "authentication.signIn.enterValidEmailAndPasswordAndMFA": "Por favor, escriba un código válido.", - "authentication.signIn.enterValidEmailAndPassword": "Por favor, escriba un correo electrónico y una contraseña válidos.", + "authentication.signIn.enterValidEmailAndPasswordAndMFA": "Por favor, escriba un código válido", + "authentication.signIn.enterValidEmailAndPassword": "Por favor, escriba un correo electrónico y una contraseña válidos", "authentication.signIn.errorGenericMessage": "Por favor inténtelo de nuevo, o comuníquese con servicio al cliente para recibir asistencia.", "authentication.signIn.error": "Hubo un error cuando usted inició sesión", "authentication.signIn.forgotPassword": "Olvidé la contraseña", diff --git a/shared-helpers/src/locales/general.json b/shared-helpers/src/locales/general.json index 10ba804f5c..50872db5f6 100644 --- a/shared-helpers/src/locales/general.json +++ b/shared-helpers/src/locales/general.json @@ -15,6 +15,8 @@ "account.viewApplications": "View applications", "account.myApplicationsSubtitle": "See lottery dates and listings for properties for which you've applied", "account.noApplications": "It looks like you haven't applied to any listings yet.", + "account.reviewTerms": "Review Terms of Use", + "account.reviewTermsHelper": "You must accept the terms of use before creating an account.", "account.signUpSaveTime.applyFaster": "Apply faster with saved application details", "account.signUpSaveTime.checkStatus": "Check on the status of an application at any time", "account.signUpSaveTime.resetPassword": "Simply reset your password if you forget it", @@ -37,6 +39,16 @@ "account.settings.placeholders.year": "YYYY", "account.settings.update": "Update", "account.settings.iconTitle": "generic user", + "account.pwdless.code": "Your code", + "account.pwdless.codeAlert": "We sent a code to %{email} to finish signing up. Be aware, the code will expire in 5 minutes.", + "account.pwdless.codeNewAlert": "A new code has been sent to %{email}. Be aware, the code will expire in 5 minutes.", + "account.pwdless.continue": "Continue", + "account.pwdless.notReceived": "Didn't receive your code?", + "account.pwdless.resend": "Resend", + "account.pwdless.resendCode": "Resend Code", + "account.pwdless.resendCodeButton": "Resend the code", + "account.pwdless.resendCodeHelper": "If there is an account made with that email, we’ll send a new code. Be aware, the code will expire in 5 minutes.", + "account.pwdless.verifyTitle": "Verify that it's you", "alert.maintenance": "This site is undergoing scheduled maintenance. We apologize for any inconvenience.", "application.ada.hearing": "For Hearing Impairments", "application.ada.label": "ADA Accessible Units", @@ -530,14 +542,19 @@ "authentication.signIn.changeYourPassword": "You can change your password", "authentication.signIn.enterLoginEmail": "Please enter your login email", "authentication.signIn.enterLoginPassword": "Please enter your login password", - "authentication.signIn.enterValidEmailAndPassword": "Please enter a valid email and password.", - "authentication.signIn.enterValidEmailAndPasswordAndMFA": "Please enter a valid code.", + "authentication.signIn.enterValidEmailAndPassword": "Please enter a valid email and password", + "authentication.signIn.enterValidEmailAndPasswordAndMFA": "Please enter a valid code", "authentication.signIn.error": "There was an error signing you in", "authentication.signIn.errorGenericMessage": "Please try again, or contact support for help.", "authentication.signIn.forgotPassword": "Forgot password?", "authentication.signIn.loginError": "Please enter a valid email address", "authentication.signIn.passwordError": "Please enter a valid password", "authentication.signIn.passwordOutdated": "Your password has expired. Please reset your password.", + "authentication.signIn.pwdless.createAccountCopy": "Sign up quicky with no need to remember any passwords.", + "authentication.signIn.pwdless.emailHelperText": "Enter your email and we'll send you a code to sign in.", + "authentication.signIn.pwdless.getCode": "Get code to sign in", + "authentication.signIn.pwdless.useCode": "Get a code instead", + "authentication.signIn.pwdless.usePassword": "Use your password instead", "authentication.signIn.success": "Welcome back, %{name}!", "authentication.signIn.youHaveToWait": "You’ll have to wait 30 minutes since the last failed attempt before trying again.", "authentication.signIn.yourAccountIsNotConfirmed": "Your account is not confirmed", @@ -917,6 +934,7 @@ "t.email": "Email", "t.emailAddressPlaceholder": "you@myemail.com", "t.filter": "Filter", + "t.finish": "Finish", "t.floor": "floor", "t.floors": "floors", "t.getDirections": "Get Directions", diff --git a/shared-helpers/src/locales/vi.json b/shared-helpers/src/locales/vi.json index 5fc73c518a..bef713010c 100644 --- a/shared-helpers/src/locales/vi.json +++ b/shared-helpers/src/locales/vi.json @@ -372,6 +372,7 @@ "authentication.createAccount.password": "Mật khẩu", "authentication.createAccount.reEnterEmail": "Nhập lại địa chỉ Email", "authentication.createAccount.reEnterPassword": "Nhập lại mật khẩu của bạn", + "authentication.createAccount.resendAnEmailTo": "Gửi lại email đến", "authentication.createAccount.resendEmailInfo": "Vui lòng nhấp vào liên kết trong email mà chúng tôi gửi cho quý vị trong vòng 24 giờ để hoàn tất việc tạo tài khoản.", "authentication.createAccount.resendTheEmail": "Gửi lại Email", @@ -386,8 +387,8 @@ "authentication.signIn.changeYourPassword": "Quý vị có thể đổi mật khẩu", "authentication.signIn.enterLoginEmail": "Vui lòng nhập email đăng nhập của quý vị", "authentication.signIn.enterLoginPassword": "Vui lòng nhập mật khẩu đăng nhập của quý vị", - "authentication.signIn.enterValidEmailAndPasswordAndMFA": "Vui lòng nhập mã hợp lệ.", - "authentication.signIn.enterValidEmailAndPassword": "Vui lòng nhập email và mật khẩu hợp lệ.", + "authentication.signIn.enterValidEmailAndPasswordAndMFA": "Vui lòng nhập mã hợp lệ", + "authentication.signIn.enterValidEmailAndPassword": "Vui lòng nhập email và mật khẩu hợp lệ", "authentication.signIn.errorGenericMessage": "Vui lòng thử lại hoặc liên lạc với bộ phận hỗ trợ để được trợ giúp.", "authentication.signIn.error": "Đã xảy ra lỗi khi quý vị đăng nhập", "authentication.signIn.forgotPassword": "Quên mật khẩu", diff --git a/shared-helpers/src/locales/zh.json b/shared-helpers/src/locales/zh.json index 1f0bed2be7..2bcfc03a44 100644 --- a/shared-helpers/src/locales/zh.json +++ b/shared-helpers/src/locales/zh.json @@ -386,8 +386,8 @@ "authentication.signIn.changeYourPassword": "您可以變更密碼", "authentication.signIn.enterLoginEmail": "請輸入您的登入電子郵件", "authentication.signIn.enterLoginPassword": "請輸入您的登入密碼", - "authentication.signIn.enterValidEmailAndPasswordAndMFA": "請輸入有效的代碼。", - "authentication.signIn.enterValidEmailAndPassword": "請輸入有效的電子郵件和密碼。", + "authentication.signIn.enterValidEmailAndPasswordAndMFA": "請輸入有效的代碼", + "authentication.signIn.enterValidEmailAndPassword": "請輸入有效的電子郵件和密碼", "authentication.signIn.errorGenericMessage": "請再試一次,或聯絡支援人員尋求協助。", "authentication.signIn.error": "您在登入時出現錯誤", "authentication.signIn.forgotPassword": "忘記密碼", diff --git a/shared-helpers/src/views/sign-in/FormSignIn.module.scss b/shared-helpers/src/views/sign-in/FormSignIn.module.scss index 3bf53b4c23..b8bad21b91 100644 --- a/shared-helpers/src/views/sign-in/FormSignIn.module.scss +++ b/shared-helpers/src/views/sign-in/FormSignIn.module.scss @@ -17,3 +17,29 @@ margin-top: var(--seeds-s6); width: 100%; } + +.sign-in-email-input { + margin-bottom: var(--seeds-s6); +} + +.sign-in-password-input { + margin-bottom: var(--seeds-s3); +} + +.sign-in-action { + margin-top: var(--seeds-s6); +} + +.create-account-copy { + padding-bottom: var(--seeds-s6); + color: var(--seeds-text-color-light); + font-size: var(--seeds-type-label-size); +} + +.pwdless-header { + margin-bottom: var(--seeds-s3); +} + +.default-header { + margin-bottom: var(--seeds-s6); +} diff --git a/shared-helpers/src/views/sign-in/FormSignIn.tsx b/shared-helpers/src/views/sign-in/FormSignIn.tsx index b46b05434a..6204feb755 100644 --- a/shared-helpers/src/views/sign-in/FormSignIn.tsx +++ b/shared-helpers/src/views/sign-in/FormSignIn.tsx @@ -1,27 +1,24 @@ -import React, { useContext } from "react" +import React from "react" import type { UseFormMethods } from "react-hook-form" -import { Field, Form, NavigationContext, t } from "@bloom-housing/ui-components" +import { useRouter } from "next/router" +import { t } from "@bloom-housing/ui-components" import { Button, Heading } from "@bloom-housing/ui-seeds" import { CardSection } from "@bloom-housing/ui-seeds/src/blocks/Card" import { FormSignInErrorBox } from "./FormSignInErrorBox" import { NetworkStatus } from "../../auth/catchNetworkError" import { BloomCard } from "../components/BloomCard" -import { useRouter } from "next/router" import { getListingRedirectUrl } from "../../utilities/getListingRedirectUrl" import styles from "./FormSignIn.module.scss" export type FormSignInProps = { control: FormSignInControl - onSubmit: (data: FormSignInValues) => void networkStatus: NetworkStatus showRegisterBtn?: boolean + children: React.ReactNode } export type FormSignInControl = { errors: UseFormMethods["errors"] - handleSubmit: UseFormMethods["handleSubmit"] - register: UseFormMethods["register"] - watch: UseFormMethods["watch"] } export type FormSignInValues = { @@ -30,18 +27,13 @@ export type FormSignInValues = { } const FormSignIn = ({ - onSubmit, + children, networkStatus, showRegisterBtn, - control: { errors, register, handleSubmit }, + control: { errors }, }: FormSignInProps) => { - const onError = () => { - window.scrollTo(0, 0) - } - const { LinkComponent } = useContext(NavigationContext) const router = useRouter() const listingIdRedirect = router.query?.listingId as string - const forgetPasswordURL = getListingRedirectUrl(listingIdRedirect, "/forgot-password") const createAccountUrl = getListingRedirectUrl(listingIdRedirect, "/create-account") return ( @@ -53,49 +45,23 @@ const FormSignIn = ({ errorMessageId={"main-sign-in"} className={styles["sign-in-error-container"]} /> - -
- - - -
- -
- -
+ {children} {showRegisterBtn && ( - + {t("authentication.createAccount.noAccount")} - + {process.env.showPwdless && ( +
+ {t("authentication.signIn.pwdless.createAccountCopy")} +
+ )} diff --git a/shared-helpers/src/views/sign-in/FormSignInDefault.tsx b/shared-helpers/src/views/sign-in/FormSignInDefault.tsx new file mode 100644 index 0000000000..783d942c49 --- /dev/null +++ b/shared-helpers/src/views/sign-in/FormSignInDefault.tsx @@ -0,0 +1,76 @@ +import React, { useContext } from "react" +import { useRouter } from "next/router" +import type { UseFormMethods } from "react-hook-form" +import { Field, Form, NavigationContext, t } from "@bloom-housing/ui-components" +import { Button } from "@bloom-housing/ui-seeds" +import { getListingRedirectUrl } from "../../utilities/getListingRedirectUrl" +import styles from "./FormSignIn.module.scss" + +export type FormSignInDefaultProps = { + control: FormSignInDefaultControl + onSubmit: (data: FormSignInDefaultValues) => void +} + +export type FormSignInDefaultValues = { + email: string + password: string +} + +export type FormSignInDefaultControl = { + errors: UseFormMethods["errors"] + handleSubmit: UseFormMethods["handleSubmit"] + register: UseFormMethods["register"] +} + +const FormSignInDefault = ({ + onSubmit, + control: { errors, register, handleSubmit }, +}: FormSignInDefaultProps) => { + const onError = () => { + window.scrollTo(0, 0) + } + const { LinkComponent } = useContext(NavigationContext) + const router = useRouter() + const listingIdRedirect = router.query?.listingId as string + const forgetPasswordURL = getListingRedirectUrl(listingIdRedirect, "/forgot-password") + + return ( +
+ + + +
+ +
+ + ) +} + +export { FormSignInDefault as default, FormSignInDefault } diff --git a/shared-helpers/src/views/sign-in/FormSignInPwdless.tsx b/shared-helpers/src/views/sign-in/FormSignInPwdless.tsx new file mode 100644 index 0000000000..cae31bf2c8 --- /dev/null +++ b/shared-helpers/src/views/sign-in/FormSignInPwdless.tsx @@ -0,0 +1,91 @@ +import React, { useContext, useState } from "react" +import { useRouter } from "next/router" +import type { UseFormMethods } from "react-hook-form" +import { Field, Form, NavigationContext, t } from "@bloom-housing/ui-components" +import { Button } from "@bloom-housing/ui-seeds" +import { getListingRedirectUrl } from "../../utilities/getListingRedirectUrl" +import styles from "./FormSignIn.module.scss" + +export type FormSignInPwdlessProps = { + control: FormSignInPwdlessControl + onSubmit: (data: FormSignInPwdlessValues) => void +} + +export type FormSignInPwdlessValues = { + email: string + password: string +} + +export type FormSignInPwdlessControl = { + errors: UseFormMethods["errors"] + handleSubmit: UseFormMethods["handleSubmit"] + register: UseFormMethods["register"] +} + +const FormSignInPwdless = ({ + onSubmit, + control: { errors, register, handleSubmit }, +}: FormSignInPwdlessProps) => { + const onError = () => { + window.scrollTo(0, 0) + } + const { LinkComponent } = useContext(NavigationContext) + const router = useRouter() + const listingIdRedirect = router.query?.listingId as string + const forgetPasswordURL = getListingRedirectUrl(listingIdRedirect, "/forgot-password") + + const [useCode, setUseCode] = useState(true) + + return ( +
+ + + {!useCode && ( + <> + + + + )} +
+ +
+
+ +
+ + ) +} + +export { FormSignInPwdless as default, FormSignInPwdless } diff --git a/sites/partners/src/pages/sign-in.tsx b/sites/partners/src/pages/sign-in.tsx index 267d88ee64..16d4793f04 100644 --- a/sites/partners/src/pages/sign-in.tsx +++ b/sites/partners/src/pages/sign-in.tsx @@ -8,6 +8,7 @@ import { AuthContext, FormSignIn, ResendConfirmationModal, + FormSignInDefault, } from "@bloom-housing/shared-helpers" import { useMutate, t } from "@bloom-housing/ui-components" import FormsLayout from "../layouts/forms" @@ -124,16 +125,6 @@ const SignIn = () => { formToRender = ( { setConfirmationStatusMessage(undefined) }, }} - /> + control={{ errors }} + > + + ) } else if (renderStep === EnumRenderStep.mfaType) { formToRender = ( diff --git a/sites/public/.env.template b/sites/public/.env.template index e91dda99a3..fc7defa67c 100644 --- a/sites/public/.env.template +++ b/sites/public/.env.template @@ -24,4 +24,5 @@ MAINTENANCE_WINDOW= GTM_KEY=GTM-KF22FJP # feature toggles -SHOW_MANDATED_ACCOUNTS=FALSE \ No newline at end of file +SHOW_MANDATED_ACCOUNTS=FALSE +SHOW_PWDLESS=FALSE diff --git a/sites/public/next.config.js b/sites/public/next.config.js index a81686633e..e96bf244dd 100644 --- a/sites/public/next.config.js +++ b/sites/public/next.config.js @@ -41,6 +41,7 @@ module.exports = withBundleAnalyzer({ cacheRevalidate: process.env.CACHE_REVALIDATE ? Number(process.env.CACHE_REVALIDATE) : 60, cloudinaryCloudName: process.env.CLOUDINARY_CLOUD_NAME, showMandatedAccounts: process.env.SHOW_MANDATED_ACCOUNTS === "TRUE", + showPwdless: process.env.SHOW_PWDLESS === "TRUE", maintenanceWindow: process.env.MAINTENANCE_WINDOW, }, i18n: { diff --git a/sites/public/src/pages/sign-in.tsx b/sites/public/src/pages/sign-in.tsx index 55f9676faf..d895d14c60 100644 --- a/sites/public/src/pages/sign-in.tsx +++ b/sites/public/src/pages/sign-in.tsx @@ -12,6 +12,8 @@ import { AuthContext, FormSignIn, ResendConfirmationModal, + FormSignInDefault, + FormSignInPwdless, } from "@bloom-housing/shared-helpers" import { UserStatus } from "../lib/constants" import { SuccessDTO } from "@bloom-housing/shared-helpers/src/types/backend-swagger" @@ -138,8 +140,6 @@ const SignIn = () => { )}
void onSubmit(data)} - control={{ register, errors, handleSubmit, watch }} networkStatus={{ content: networkStatusContent, type: networkStatusType, @@ -150,7 +150,20 @@ const SignIn = () => { }, }} showRegisterBtn={true} - /> + control={{ errors }} + > + {process.env.showPwdless ? ( + void onSubmit(data)} + control={{ register, errors, handleSubmit }} + /> + ) : ( + void onSubmit(data)} + control={{ register, errors, handleSubmit }} + /> + )} +
{signUpCopy && (
diff --git a/sites/public/src/pages/verify.tsx b/sites/public/src/pages/verify.tsx new file mode 100644 index 0000000000..3a5d8e023d --- /dev/null +++ b/sites/public/src/pages/verify.tsx @@ -0,0 +1,119 @@ +import React, { useEffect, useState } from "react" +import { useForm } from "react-hook-form" +import { Button, Alert, Message, Dialog } from "@bloom-housing/ui-seeds" +import { CardSection } from "@bloom-housing/ui-seeds/src/blocks/Card" +import { + DialogContent, + DialogFooter, + DialogHeader, +} from "@bloom-housing/ui-seeds/src/overlays/Dialog" +import { Field, Form, t, SiteAlert } from "@bloom-housing/ui-components" +import { + PageView, + pushGtmEvent, + useCatchNetworkError, + BloomCard, +} from "@bloom-housing/shared-helpers" +import { UserStatus } from "../lib/constants" +import FormsLayout from "../layouts/forms" +import styles from "../../styles/verify.module.scss" + +const Verify = () => { + // eslint-disable-next-line @typescript-eslint/unbound-method + const { register, handleSubmit, errors } = useForm() + const { determineNetworkError } = useCatchNetworkError() + + const [isModalOpen, setIsModalOpen] = useState(false) + const [alertMessage, setAlertMessage] = useState( + t("account.pwdless.codeAlert", { email: "example@email.com" }) + ) // This copy will change based on coming from the sign in flow or create account flow + + useEffect(() => { + pushGtmEvent({ + event: "pageView", + pageTitle: "Verify", + status: UserStatus.NotLoggedIn, + }) + }, []) + + const onSubmit = (data: { code: string }) => { + // const { code } = data + + try { + // Attempt to either create an account or sign in + } catch (error) { + const { status } = error.response || {} + determineNetworkError(status, error) + // "The code you've used is invalid or expired" + } + } + + return ( + + + <> + + + + {!!Object.keys(errors).length && ( + + {t("errors.errorsToResolve")} + + )} + + {alertMessage} + +
+ +
+ +
+ + + +
+ +
+ setIsModalOpen(false)}> + {t("account.pwdless.resendCode")} + {t("account.pwdless.resendCodeHelper")} + + + + + +
+ ) +} + +export { Verify as default, Verify } diff --git a/sites/public/styles/verify.module.scss b/sites/public/styles/verify.module.scss new file mode 100644 index 0000000000..791692ac81 --- /dev/null +++ b/sites/public/styles/verify.module.scss @@ -0,0 +1,33 @@ +.verify-resend-link { + margin-bottom: var(--seeds-s6); +} + +.verify-message { + max-width: 100%; + margin-bottom: var(--seeds-s4); +} + +.verify-error { + margin-bottom: var(--seeds-s4); +} + +.verify-code { + margin-bottom: 0; +} + +.terms-card { + overflow-y: auto; + max-height: calc(100vh - 24rem); + min-height: 30rem; + position: relative; + margin-bottom: var(--seeds-s12); + @media (max-width: theme("screens.sm")) { + height: 100%; + max-height: 100%; + margin-bottom: 0; + } +} + +.terms-form { + margin-bottom: var(--seeds-12); +}