From ca4c8434ea9b9ee01627897d0ef193af1a037e2f Mon Sep 17 00:00:00 2001
From: Emily Jablonski <65367387+emilyjablonski@users.noreply.github.com>
Date: Tue, 19 Mar 2024 12:40:42 -0600
Subject: [PATCH] feat: pwdless sign in flow (#3960)
---
api/prisma/seed-dev.ts | 5 +-
api/src/views/partials/user-name.hbs | 2 +-
api/src/views/single-use-code.hbs | 1 -
shared-helpers/src/auth/AuthContext.ts | 12 ++
shared-helpers/src/auth/catchNetworkError.ts | 9 ++
shared-helpers/src/locales/general.json | 8 +-
.../src/views/sign-in/FormSignInPwdless.tsx | 12 +-
sites/public/src/pages/sign-in.tsx | 44 ++++++-
sites/public/src/pages/verify.tsx | 114 +++++++++++++-----
sites/public/styles/overrides.scss | 3 +
sites/public/styles/verify.module.scss | 23 ++--
11 files changed, 178 insertions(+), 55 deletions(-)
diff --git a/api/prisma/seed-dev.ts b/api/prisma/seed-dev.ts
index 357118d71a..73139b5bb9 100644
--- a/api/prisma/seed-dev.ts
+++ b/api/prisma/seed-dev.ts
@@ -45,7 +45,10 @@ export const devSeeding = async (
jurisdictionName?: string,
) => {
const jurisdiction = await prismaClient.jurisdictions.create({
- data: jurisdictionFactory(jurisdictionName),
+ data: {
+ ...jurisdictionFactory(jurisdictionName),
+ allowSingleUseCodeLogin: true,
+ },
});
await prismaClient.userAccounts.create({
data: await userFactory({
diff --git a/api/src/views/partials/user-name.hbs b/api/src/views/partials/user-name.hbs
index 5f8a7f3e24..52e44c0cf3 100644
--- a/api/src/views/partials/user-name.hbs
+++ b/api/src/views/partials/user-name.hbs
@@ -1,4 +1,4 @@
{{user.firstName}}
{{#if user.middleName}}
{{user.middleName}}
-{{/if}} {{user.lastName}}
+{{/if}} {{user.lastName}}
\ No newline at end of file
diff --git a/api/src/views/single-use-code.hbs b/api/src/views/single-use-code.hbs
index 857f09b468..026666eb37 100644
--- a/api/src/views/single-use-code.hbs
+++ b/api/src/views/single-use-code.hbs
@@ -2,7 +2,6 @@
{{t "singleUseCodeEmail.message" singleUseCodeOptions }}
-
{{t "singleUseCodeEmail.singleUseCode" singleUseCodeOptions}}
diff --git a/shared-helpers/src/auth/AuthContext.ts b/shared-helpers/src/auth/AuthContext.ts
index 6bf148db37..d6e141d290 100644
--- a/shared-helpers/src/auth/AuthContext.ts
+++ b/shared-helpers/src/auth/AuthContext.ts
@@ -31,6 +31,7 @@ import {
UserCreate,
UserService,
serviceOptions,
+ SuccessDTO,
} from "../types/backend-swagger"
import { getListingRedirectUrl } from "../utilities/getListingRedirectUrl"
@@ -74,6 +75,7 @@ type ContextProps = {
mfaType: MfaType,
phoneNumber?: string
) => Promise
+ requestSingleUseCode: (email: string) => Promise
loginViaSingleUseCode: (email: string, singleUseCode: string) => Promise
}
@@ -360,6 +362,16 @@ export const AuthProvider: FunctionComponent = ({ child
dispatch(stopLoading())
}
},
+ requestSingleUseCode: async (email) => {
+ dispatch(startLoading())
+ try {
+ return await authService?.requestSingleUseCode({
+ body: { email },
+ })
+ } finally {
+ dispatch(stopLoading())
+ }
+ },
}
return createElement(AuthContext.Provider, { value: contextValues }, children)
}
diff --git a/shared-helpers/src/auth/catchNetworkError.ts b/shared-helpers/src/auth/catchNetworkError.ts
index e61114a421..a3ee02e457 100644
--- a/shared-helpers/src/auth/catchNetworkError.ts
+++ b/shared-helpers/src/auth/catchNetworkError.ts
@@ -29,6 +29,7 @@ export type NetworkErrorReset = () => void
export enum NetworkErrorMessage {
PasswordOutdated = "but password is no longer valid",
MfaUnauthorized = "mfaUnauthorized",
+ SingleUseCodeUnauthorized = "singleUseCodeUnauthorized",
}
/**
@@ -54,6 +55,14 @@ export const useCatchNetworkError = () => {
}),
error,
})
+ } else if (message === NetworkErrorMessage.SingleUseCodeUnauthorized) {
+ setNetworkError({
+ title: t("authentication.signIn.pwdless.error"),
+ description: t("authentication.signIn.afterFailedAttempts", {
+ count: error?.response?.data?.failureCountRemaining || 5,
+ }),
+ error,
+ })
} else {
setNetworkError({
title: t("authentication.signIn.enterValidEmailAndPassword"),
diff --git a/shared-helpers/src/locales/general.json b/shared-helpers/src/locales/general.json
index 50872db5f6..c4b57ec052 100644
--- a/shared-helpers/src/locales/general.json
+++ b/shared-helpers/src/locales/general.json
@@ -40,14 +40,16 @@
"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.createMessage": "We sent a code to %{email} to finish signing up. Be aware, the code will expire in 5 minutes.",
+ "account.pwdless.loginMessage": "If there is an account made with %{email}, we’ll send a code within 5 minutes. If you don’t receive a code, sign in with your password and confirm your email address under account settings.",
"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.resendCodeHelper": "If there is an account made with %{email}, we’ll send a new code. Be aware, the code will expire in 5 minutes.",
+ "account.pwdless.signInWithYourPassword": "Sign in with your password",
"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",
@@ -552,6 +554,7 @@
"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.error": "The code you've used is invalid or expired.",
"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",
@@ -948,6 +951,7 @@
"t.lastUpdated": "Last Updated",
"t.less": "Less",
"t.letter": "Letter",
+ "t.loading": "Loading",
"t.loginIsRequired": "Login is required to view this page.",
"t.menu": "Menu",
"t.minimumIncome": "Minimum Income",
diff --git a/shared-helpers/src/views/sign-in/FormSignInPwdless.tsx b/shared-helpers/src/views/sign-in/FormSignInPwdless.tsx
index cae31bf2c8..03acb314ae 100644
--- a/shared-helpers/src/views/sign-in/FormSignInPwdless.tsx
+++ b/shared-helpers/src/views/sign-in/FormSignInPwdless.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useState } from "react"
+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"
@@ -9,6 +9,8 @@ import styles from "./FormSignIn.module.scss"
export type FormSignInPwdlessProps = {
control: FormSignInPwdlessControl
onSubmit: (data: FormSignInPwdlessValues) => void
+ useCode: boolean
+ setUseCode: React.Dispatch>
}
export type FormSignInPwdlessValues = {
@@ -25,6 +27,8 @@ export type FormSignInPwdlessControl = {
const FormSignInPwdless = ({
onSubmit,
control: { errors, register, handleSubmit },
+ useCode,
+ setUseCode,
}: FormSignInPwdlessProps) => {
const onError = () => {
window.scrollTo(0, 0)
@@ -34,15 +38,13 @@ const FormSignInPwdless = ({
const listingIdRedirect = router.query?.listingId as string
const forgetPasswordURL = getListingRedirectUrl(listingIdRedirect, "/forgot-password")
- const [useCode, setUseCode] = useState(true)
-
return (