From e176355df8a48d60eabaa5efcc7e092c3b61bbaa Mon Sep 17 00:00:00 2001 From: Jared White Date: Fri, 4 Oct 2024 15:01:46 -0700 Subject: [PATCH 1/2] fix: restore toasts in a way which prevents infinite rerenders (#4302) * fix: restore toasts in a way which prevents infinite rerenders * fix: linting * fix: use a ref to hold a stable reference for addToast in effects * chore: add comment --------- Co-authored-by: Emily Jablonski <65367387+emilyjablonski@users.noreply.github.com> (cherry picked from commit 4445b970249a973436de63ff9b5499fba7d67018) --- shared-helpers/src/auth/RequireLogin.tsx | 8 +++++--- shared-helpers/src/utilities/MessageContext.ts | 18 +++++++++++++++++- .../components/account/ConfirmationModal.tsx | 11 ++++++----- .../src/pages/applications/review/summary.tsx | 10 ++++++---- .../applications/start/choose-language.tsx | 10 ++++++---- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/shared-helpers/src/auth/RequireLogin.tsx b/shared-helpers/src/auth/RequireLogin.tsx index 89b97f0089..633443abdf 100644 --- a/shared-helpers/src/auth/RequireLogin.tsx +++ b/shared-helpers/src/auth/RequireLogin.tsx @@ -1,7 +1,7 @@ import React, { FunctionComponent, useContext, useEffect, useState } from "react" import { NavigationContext } from "@bloom-housing/ui-components" import { AuthContext } from "./AuthContext" -import { MessageContext } from "../utilities/MessageContext" +import { useToastyRef } from "../utilities/MessageContext" // See https://github.com/Microsoft/TypeScript/issues/14094 type Without = { [P in Exclude]?: never } @@ -31,7 +31,7 @@ const RequireLogin: FunctionComponent = ({ }) => { const { router } = useContext(NavigationContext) const { profile, initialStateLoaded } = useContext(AuthContext) - const { addToast } = useContext(MessageContext) + const toastyRef = useToastyRef() const [hasTerms, setHasTerms] = useState(false) // Parse just the pathname portion of the signInPath (in case we want to pass URL params) @@ -56,6 +56,8 @@ const RequireLogin: FunctionComponent = ({ }, [profile]) useEffect(() => { + const { addToast } = toastyRef.current + if (loginRequiredForPath && initialStateLoaded && !profile) { addToast(signInMessage, { variant: "primary" }) void router.push(signInPath) @@ -73,7 +75,7 @@ const RequireLogin: FunctionComponent = ({ signInMessage, termsPath, hasTerms, - addToast, + toastyRef, ]) if ( diff --git a/shared-helpers/src/utilities/MessageContext.ts b/shared-helpers/src/utilities/MessageContext.ts index e778c586bd..78a533e9d0 100644 --- a/shared-helpers/src/utilities/MessageContext.ts +++ b/shared-helpers/src/utilities/MessageContext.ts @@ -1,4 +1,11 @@ -import React, { FunctionComponent, createContext, createElement, useState, useRef } from "react" +import React, { + FunctionComponent, + createContext, + createElement, + useContext, + useState, + useRef, +} from "react" import { CommonMessageProps } from "@bloom-housing/ui-seeds/src/blocks/shared/CommonMessage" // TODO: this should be exportable from seeds directly @@ -49,3 +56,12 @@ export const MessageProvider: FunctionComponent = ({ ch children ) } + +/** + * Use the current value of a ref within `useEffect` so you can pass the ref to the dependencies + * array. Otherwise, the effect will constantly rerun because the context alone isn't a stable ref. + * File this one in the "Weird React" category! + */ +export const useToastyRef = () => { + return useRef(useContext(MessageContext)) +} diff --git a/sites/public/src/components/account/ConfirmationModal.tsx b/sites/public/src/components/account/ConfirmationModal.tsx index d520f6092a..0eb0da0c4e 100644 --- a/sites/public/src/components/account/ConfirmationModal.tsx +++ b/sites/public/src/components/account/ConfirmationModal.tsx @@ -1,6 +1,6 @@ import { t, Form, Field } from "@bloom-housing/ui-components" import { Button, Dialog } from "@bloom-housing/ui-seeds" -import { AuthContext, MessageContext, emailRegex } from "@bloom-housing/shared-helpers" +import { AuthContext, useToastyRef, emailRegex } from "@bloom-housing/shared-helpers" import { useRouter } from "next/router" import { useContext, useEffect, useRef, useState } from "react" import { useForm } from "react-hook-form" @@ -10,7 +10,7 @@ export interface ConfirmationModalProps {} const ConfirmationModal = () => { const { resendConfirmation, profile, confirmAccount } = useContext(AuthContext) - const { addToast } = useContext(MessageContext) + const toastyRef = useToastyRef() const [openModal, setOpenModal] = useState(false) const router = useRouter() @@ -23,6 +23,8 @@ const ConfirmationModal = () => { email.current = watch("email", "") const onSubmit = async (email) => { + const { addToast } = toastyRef.current + try { const listingId = router.query?.listingId as string await resendConfirmation(email, listingId) @@ -42,6 +44,7 @@ const ConfirmationModal = () => { } useEffect(() => { + const { addToast } = toastyRef.current const redirectUrl = router.query?.redirectUrl as string const listingId = router.query?.listingId as string @@ -71,9 +74,7 @@ const ConfirmationModal = () => { } }) } - // This ensures useEffect is called only once - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [router, profile]) + }, [router, profile, toastyRef]) return ( { const router = useRouter() const { profile, applicationsService } = useContext(AuthContext) - const { addToast } = useContext(MessageContext) + const toastyRef = useToastyRef() const [validationError, setValidationError] = useState(false) const { conductor, application, listing } = useFormConductor("summary") let currentPageSection = 4 @@ -51,6 +51,8 @@ const ApplicationSummary = () => { }, [profile]) useEffect(() => { + const { addToast } = toastyRef.current + if (listing && router.isReady) { const currentDate = dayjs() if ( @@ -58,11 +60,11 @@ const ApplicationSummary = () => { listing?.status !== ListingsStatusEnum.active || (listing?.applicationDueDate && currentDate > dayjs(listing.applicationDueDate)) ) { - // addToast(t("listings.applicationsClosedRedirect"), { variant: "alert" }) + addToast(t("listings.applicationsClosedRedirect"), { variant: "alert" }) void router.push(`/${router.locale}/listing/${listing?.id}/${listing.urlSlug}`) } } - }, [listing, router, addToast]) + }, [listing, router, toastyRef]) useEffect(() => { conductor.application.reachedReviewStep = true diff --git a/sites/public/src/pages/applications/start/choose-language.tsx b/sites/public/src/pages/applications/start/choose-language.tsx index 9a45a8b352..257a1c9e94 100644 --- a/sites/public/src/pages/applications/start/choose-language.tsx +++ b/sites/public/src/pages/applications/start/choose-language.tsx @@ -7,7 +7,7 @@ import { PageView, pushGtmEvent, AuthContext, - MessageContext, + useToastyRef, CustomIconMap, } from "@bloom-housing/shared-helpers" import { @@ -54,7 +54,7 @@ const ApplicationChooseLanguage = () => { const [listing, setListing] = useState(null) const context = useContext(AppSubmissionContext) const { initialStateLoaded, profile, listingsService } = useContext(AuthContext) - const { addToast } = useContext(MessageContext) + const toastyRef = useToastyRef() const { conductor } = context const listingId = router.query.listingId @@ -86,6 +86,8 @@ const ApplicationChooseLanguage = () => { }, [router, conductor, context, listingId, initialStateLoaded, profile, listingsService]) useEffect(() => { + const { addToast } = toastyRef.current + if (listing && router.isReady) { const currentDate = dayjs() if ( @@ -93,11 +95,11 @@ const ApplicationChooseLanguage = () => { (router?.query?.preview !== "true" && listing?.status !== ListingsStatusEnum.active) || (listing?.applicationDueDate && currentDate > dayjs(listing.applicationDueDate)) ) { - // addToast(t("listings.applicationsClosedRedirect"), { variant: "alert" }) + addToast(t("listings.applicationsClosedRedirect"), { variant: "alert" }) void router.push(`/${router.locale}/listing/${listing?.id}/${listing?.urlSlug}`) } } - }, [listing, router, addToast]) + }, [listing, router, toastyRef]) const imageUrl = listing?.assets ? imageUrlFromListing(listing, parseInt(process.env.listingPhotoSize))[0] From 13e6e468b1f64a8468788a0b9755060addd3a504 Mon Sep 17 00:00:00 2001 From: Jared White Date: Tue, 19 Nov 2024 09:14:17 -0800 Subject: [PATCH 2/2] feat: update toast messages for Forgot Password (#4466) (cherry picked from commit 2edb70f8ad91cc2df7897b2197ffcc2233013a91) --- shared-helpers/src/locales/es.json | 2 +- shared-helpers/src/locales/general.json | 2 +- shared-helpers/src/locales/tl.json | 2 +- shared-helpers/src/locales/vi.json | 2 +- shared-helpers/src/locales/zh.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/shared-helpers/src/locales/es.json b/shared-helpers/src/locales/es.json index 9f399bc74c..bb30991ce5 100644 --- a/shared-helpers/src/locales/es.json +++ b/shared-helpers/src/locales/es.json @@ -458,7 +458,7 @@ "authentication.forgotPassword.errors.passwordTooWeak": "La contraseña es demasiado débil. Debe tener al menos 12 caracteres e incluir al menos una letra minúscula, una letra mayúscula, un número y un carácter especial (#?!@$%^&*-).", "authentication.forgotPassword.errors.tokenExpired": "El token de restablecimiento de contraseña caducó. Por favor, solicite uno nuevo.", "authentication.forgotPassword.errors.tokenMissing": "El token no fue encontrado. Por favor, solicite uno nuevo.", - "authentication.forgotPassword.message": "Si hay una cuenta creada con ese correo electrónico, recibirás un correo electrónico con un enlace para restablecer tu contraseña.", + "authentication.forgotPassword.message": "Si hay una cuenta creada con ese correo electrónico, recibirás un correo electrónico con un enlace para restablecer tu contraseña. El enlace de reinicio es válido por 1 hora.", "authentication.forgotPassword.passwordConfirmation": "Confirmación de contraseña", "authentication.forgotPassword.sendEmail": "Enviar correo electrónico", "authentication.signIn.accountHasBeenLocked": "Por razones de seguridad, esta cuenta ha sido bloqueada.", diff --git a/shared-helpers/src/locales/general.json b/shared-helpers/src/locales/general.json index 75bdfdaff2..2a6d6bd826 100644 --- a/shared-helpers/src/locales/general.json +++ b/shared-helpers/src/locales/general.json @@ -449,7 +449,7 @@ "authentication.forgotPassword.errors.passwordTooWeak": "Password is too weak. Must be at least 12 characters and include at least one lowercase letter, one uppercase letter, one number, and one special character (#?!@$%^&*-).", "authentication.forgotPassword.errors.tokenExpired": "Reset password token expired. Please request for a new one.", "authentication.forgotPassword.errors.tokenMissing": "Token not found. Please request for a new one.", - "authentication.forgotPassword.message": "If there is an account made with that email, you'll receive an email with a link to reset your password.", + "authentication.forgotPassword.message": "If there is an account made with that email, you'll receive an email with a link to reset your password. The reset link is valid for 1 hour.", "authentication.forgotPassword.passwordConfirmation": "Password Confirmation", "authentication.forgotPassword.sendEmail": "Send email", "authentication.signIn.accountHasBeenLocked": "For security reasons, this account has been locked.", diff --git a/shared-helpers/src/locales/tl.json b/shared-helpers/src/locales/tl.json index 83df4e0b1d..65f0560dde 100644 --- a/shared-helpers/src/locales/tl.json +++ b/shared-helpers/src/locales/tl.json @@ -457,7 +457,7 @@ "authentication.forgotPassword.errors.passwordTooWeak": "Masyadong mahina ang password. Dapat ay hindi bababa sa 12 character at may kasamang hindi bababa sa isang maliit na titik, isang malaking titik, isang numero, at isang espesyal na character (#?!@$%^&*-).", "authentication.forgotPassword.errors.tokenExpired": "Nag-expire na ang token ng pag-reset ng password. Humiling ng bago.", "authentication.forgotPassword.errors.tokenMissing": "Hindi nahanap ang token. Humiling ng bago.", - "authentication.forgotPassword.message": "Kung may account na ginawa gamit ang email na iyon, makakatanggap ka ng email na may link para i-reset ang iyong password.", + "authentication.forgotPassword.message": "Kung may account na ginawa gamit ang email na iyon, makakatanggap ka ng email na may link para i-reset ang iyong password. Ang link sa pag-reset ay may bisa sa loob ng 1 oras.", "authentication.forgotPassword.passwordConfirmation": "Pagkumpirma ng Password", "authentication.forgotPassword.sendEmail": "Magpadala ng email", "authentication.signIn.accountHasBeenLocked": "Para sa mga kadahilanang pangseguridad, ang account na ito isinara na.", diff --git a/shared-helpers/src/locales/vi.json b/shared-helpers/src/locales/vi.json index cb9fab5919..55ed39e002 100644 --- a/shared-helpers/src/locales/vi.json +++ b/shared-helpers/src/locales/vi.json @@ -458,7 +458,7 @@ "authentication.forgotPassword.errors.passwordTooWeak": "Mật khẩu quá yếu. Phải có ít nhất 12 ký tự và bao gồm ít nhất một chữ cái viết thường, một chữ cái viết hoa, một số và một ký tự đặc biệt (#?!@$%^&*-).", "authentication.forgotPassword.errors.tokenExpired": "Mã thông báo đặt lại mật khẩu đã hết hạn. Vui lòng yêu cầu mã mới.", "authentication.forgotPassword.errors.tokenMissing": "Không tìm thấy mã thông báo. Vui lòng yêu cầu mã mới.", - "authentication.forgotPassword.message": "Nếu có tài khoản được tạo bằng email đó, bạn sẽ nhận được email có liên kết để đặt lại mật khẩu của mình.", + "authentication.forgotPassword.message": "Nếu có tài khoản được tạo bằng email đó, bạn sẽ nhận được email có liên kết để đặt lại mật khẩu của mình. Liên kết đặt lại có hiệu lực trong 1 giờ.", "authentication.forgotPassword.passwordConfirmation": "Xác nhận mật khẩu", "authentication.forgotPassword.sendEmail": "Gửi email", "authentication.signIn.accountHasBeenLocked": "Vì lý do bảo mật, tài khoản này đã bị khóa.", diff --git a/shared-helpers/src/locales/zh.json b/shared-helpers/src/locales/zh.json index 7d8ed27463..19444ff04a 100644 --- a/shared-helpers/src/locales/zh.json +++ b/shared-helpers/src/locales/zh.json @@ -458,7 +458,7 @@ "authentication.forgotPassword.errors.passwordTooWeak": "密碼強度太弱。必須至少 12 個字符,並且至少包含 1 個小寫字母、1 個大寫字母、1 個數字和 1 個特殊字符 (#?!@$%^&*-)。", "authentication.forgotPassword.errors.tokenExpired": "重設密碼權杖已到期。請要求新的權杖。", "authentication.forgotPassword.errors.tokenMissing": "找不到權杖。請要求新的權杖。", - "authentication.forgotPassword.message": "如果使用该电子邮件创建了帐户,您将收到一封包含重置密码链接的电子邮件。", + "authentication.forgotPassword.message": "如果使用该电子邮件创建了帐户,您将收到一封包含重置密码链接的电子邮件。 重置链接有效期为1小时。", "authentication.forgotPassword.passwordConfirmation": "确认密码", "authentication.forgotPassword.sendEmail": "傳送電子郵件", "authentication.signIn.accountHasBeenLocked": "基於安全原因,此帳戶已遭到鎖定。",