From 3b7d0c4e83af9e25c0e95a58fdcc7b34c51c174f Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Mon, 19 Feb 2024 21:04:45 -0500 Subject: [PATCH 01/40] multi-step modal login/signup flow --- client/src/components/EmailAlertSignup.tsx | 42 ++---- client/src/components/EmailInput.tsx | 79 ++++++++++ client/src/components/PasswordInput.tsx | 31 ++-- client/src/components/UserContext.tsx | 15 +- client/src/components/UserSettingField.tsx | 21 +-- client/src/components/UserTypeInput.tsx | 152 ++++++++++++++++++++ client/src/containers/ResetPasswordPage.tsx | 16 ++- client/src/styles/EmailInput.scss | 61 ++++++++ client/src/styles/Login.scss | 60 +++----- client/src/styles/Password.scss | 25 +++- client/src/styles/UserTypeInput.scss | 69 +++++++++ client/src/styles/_input.scss | 1 - client/src/util/helpers.ts | 20 ++- 13 files changed, 487 insertions(+), 105 deletions(-) create mode 100644 client/src/components/EmailInput.tsx create mode 100644 client/src/components/UserTypeInput.tsx create mode 100644 client/src/styles/EmailInput.scss create mode 100644 client/src/styles/UserTypeInput.scss diff --git a/client/src/components/EmailAlertSignup.tsx b/client/src/components/EmailAlertSignup.tsx index b25a6fc73..1f62d7663 100644 --- a/client/src/components/EmailAlertSignup.tsx +++ b/client/src/components/EmailAlertSignup.tsx @@ -10,7 +10,6 @@ import "styles/EmailAlertSignup.css"; import { JustfixUser } from "state-machine"; import AuthClient from "./AuthClient"; import { AlertIconOutline, SubscribedIcon } from "./Icons"; -import Modal from "./Modal"; type BuildingSubscribeProps = withI18nProps & { bbl: string; @@ -91,7 +90,7 @@ const EmailAlertSignupWithoutI18n = (props: EmailAlertProps) => { const { bbl, housenumber, streetname, zip, boro } = props; const userContext = useContext(UserContext); const { user } = userContext; - const [showVerifyModal, setShowVerifyModal] = useState(false); + const [loginRegisterInProgress, setLoginRegisterInProgress] = useState(false); return ( <> @@ -114,8 +113,10 @@ const EmailAlertSignupWithoutI18n = (props: EmailAlertProps) => { )}
- {!user ? ( - + {user && !loginRegisterInProgress ? ( + + ) : ( + <>
Each weekly email includes HPD Complaints, HPD Violations, and Eviction @@ -123,41 +124,16 @@ const EmailAlertSignupWithoutI18n = (props: EmailAlertProps) => {
{ userContext.subscribe(bbl, housenumber, streetname, zip, boro, user); - !user.verified && setShowVerifyModal(true); }} /> -
- ) : ( - + )}
- setShowVerifyModal(false)} - > - Verify your email to start receiving updates - {i18n._( - t`Click the link we sent to ${user?.email}. It may take a few minutes to arrive.` - )} -
-
- - Once your email has been verified, you’ll be signed up for Data Updates. - -
-
- -
)} diff --git a/client/src/components/EmailInput.tsx b/client/src/components/EmailInput.tsx new file mode 100644 index 000000000..a4f1b0152 --- /dev/null +++ b/client/src/components/EmailInput.tsx @@ -0,0 +1,79 @@ +import { ChangeEvent } from "react"; +import { Trans, t } from "@lingui/macro"; +import { I18n } from "@lingui/core"; +import { withI18n } from "@lingui/react"; +import { AlertIcon } from "./Icons"; + +import "styles/EmailInput.css"; +import "styles/_input.scss"; + +type EmailInputProps = { + i18n: I18n; + email: string; + error: boolean; + setError: React.Dispatch>; + onChange: (e: ChangeEvent) => void; + required?: boolean; +}; + +const EmailInputWithoutI18n = (props: EmailInputProps) => { + const { i18n, email, error, setError, onChange, required } = props; + + const isBadEmailFormat = () => { + /* valid email regex rules + alpha numeric characters are ok, upper/lower case agnostic + username: leading \_ ok, chars \_\.\-\+ ok in all other positions + domain name: chars \.\- ok as long as not leading. must end in a \. and at least two alphabet chars */ + const pattern = + "^([a-zA-Z0-9_]+[a-zA-Z0-9+_.-]+@[a-zA-Z0-9]+[a-zA-Z0-9.-]+[a-zA-Z0-9]+.[a-zA-Z]{2,})$"; + const input = document.getElementById("email-input") as HTMLElement; + const inputValue = (input as HTMLInputElement).value; + + // HTML input element has loose email validation requirements, so we check the input against a custom regex + const passStrictRegex = inputValue.match(pattern); + const passAutoValidation = document.querySelectorAll("input:invalid").length === 0; + + if (!passAutoValidation || !passStrictRegex) { + setError(true); + input.className = input.className + " invalid"; + } else { + setError(false); + input.className = input.className.split(" ")[0]; + } + }; + + return ( + <> +
+ +
+ {error && ( +
+ + + Please enter a valid email address. + +
+ )} +
+ +
+ + ); +}; + +const EmailInput = withI18n()(EmailInputWithoutI18n); + +export default EmailInput; diff --git a/client/src/components/PasswordInput.tsx b/client/src/components/PasswordInput.tsx index c7a663a34..128bc58a2 100644 --- a/client/src/components/PasswordInput.tsx +++ b/client/src/components/PasswordInput.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-useless-escape */ -import React, { Fragment, useState } from "react"; +import React, { ChangeEvent, Fragment, useState } from "react"; import "styles/Password.css"; import "styles/_input.scss"; @@ -33,22 +33,33 @@ export const validatePassword = (password: string) => { type PasswordInputProps = { i18n: I18n; labelText: string; + password: string; username?: string; - onChange?: (password: string) => void; + onChange: (e: ChangeEvent) => void; + setError?: React.Dispatch>; showPasswordRules?: boolean; showForgotPassword?: boolean; }; const PasswordInputWithoutI18n = (props: PasswordInputProps) => { const { account } = createWhoOwnsWhatRoutePaths(); - - const { i18n, labelText, username, onChange, showPasswordRules, showForgotPassword } = props; - const [password, setPassword] = useState(""); + const { + i18n, + labelText, + username, + password, + setError, + onChange, + showPasswordRules, + showForgotPassword, + } = props; const [showPassword, setShowPassword] = useState(false); - const handlePasswordChange = (e: React.ChangeEvent) => { - setPassword(e.target.value); - if (onChange) onChange(e.target.value); + const badPasswordFormat = () => { + const input = document.getElementById("password-input") as HTMLElement; + const inputValue = (input as HTMLInputElement).value; + const isValid = validatePassword(inputValue); + setError && setError(!isValid); }; return ( @@ -76,8 +87,10 @@ const PasswordInputWithoutI18n = (props: PasswordInputProps) => {
*/} ); diff --git a/client/src/components/UserTypeInput.tsx b/client/src/components/UserTypeInput.tsx new file mode 100644 index 000000000..96066cc13 --- /dev/null +++ b/client/src/components/UserTypeInput.tsx @@ -0,0 +1,152 @@ +import { ChangeEvent, useState } from "react"; +import { Trans, t } from "@lingui/macro"; +import { I18n } from "@lingui/core"; +import { withI18n } from "@lingui/react"; +import { AlertIcon } from "./Icons"; + +type UserTypeInputProps = { + i18n: I18n; + userType: string; + error: boolean; + setError: React.Dispatch>; + setUserType: React.Dispatch>; + onChange: (e: ChangeEvent) => void; + required?: boolean; +}; + +const UserTypeInputWithoutI18n = (props: UserTypeInputProps) => { + const { i18n, userType, error, setError, setUserType, onChange, required } = props; + + const [activeRadio, setActiveRadio] = useState(""); + + const handleChange = (e: ChangeEvent) => { + const value = e.target.value; + setActiveRadio(value); + if (value === USER_TYPES.other) { + setUserType(""); + } else { + onChange(e); + setError(false); + } + }; + + const missingOtherText = () => { + const input = document.getElementById("user-type-input-other-text") as HTMLElement; + const inputValue = (input as HTMLInputElement).value; + + if (!inputValue) { + setError(true); + input.className = input.className + " invalid"; + } else { + setError(false); + input.className = input.className.split(" ")[0]; + } + }; + + const USER_TYPES = { + tenant: i18n._(t`Tenant`), + organizer: i18n._(t`Organizer`), + advocate: i18n._(t`Advocate`), + legal: i18n._(t`Legal Worker`), + government: i18n._(t`Government Worker (Non-Lawyer)`), + other: i18n._(t`Other`), + }; + + return ( +
+
+ + + + + + + + + + + + +
+ {activeRadio === USER_TYPES.other && ( +
+ {error && ( + + + Please enter a response. + + )} + +
+ )} +
+ ); +}; + +const UserTypeInput = withI18n()(UserTypeInputWithoutI18n); + +export default UserTypeInput; diff --git a/client/src/containers/ResetPasswordPage.tsx b/client/src/containers/ResetPasswordPage.tsx index 04e062e46..d84d46714 100644 --- a/client/src/containers/ResetPasswordPage.tsx +++ b/client/src/containers/ResetPasswordPage.tsx @@ -8,6 +8,7 @@ import { Trans, t } from "@lingui/macro"; import { UserContext } from "components/UserContext"; import PasswordInput from "components/PasswordInput"; +import { useInput } from "util/helpers"; const ResetPasswordPage = withI18n()((props: withI18nProps) => { const { i18n } = props; @@ -15,8 +16,8 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { const params = new URLSearchParams(search); const [requestSent, setRequestSent] = React.useState(false); - const [value, setValue] = React.useState(""); const userContext = useContext(UserContext); + const { value: password, onChange: onChangePassword } = useInput(""); const delaySeconds = 5; const baseUrl = window.location.origin; @@ -38,7 +39,7 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await userContext.resetPassword(params.get("token") || "", value); + await userContext.resetPassword(params.get("token") || "", password); setRequestSent(true); }; @@ -49,13 +50,16 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { {!requestSent ? ( <> Reset your password -
+ - + ) : ( diff --git a/client/src/styles/EmailInput.scss b/client/src/styles/EmailInput.scss new file mode 100644 index 000000000..80d415425 --- /dev/null +++ b/client/src/styles/EmailInput.scss @@ -0,0 +1,61 @@ +@import "_vars.scss"; +@import "_typography.scss"; + +.email-input-label { + display: flex; + align-items: center; + justify-content: space-between; + + label { + @include desktop-eyebrow(); + // vars marked !important bc typography mixin is not available for inconsolata + font-weight: 600 !important; + font-family: $wow-font !important; + font-size: 1.3rem !important; + color: $justfix-black; + + text-align: left; + margin-top: 1rem; + margin-bottom: 0.5rem; + } + + a { + color: $justfix-black; + font-size: 1.3rem; + } +} + +.email-input { + display: flex; + + input { + flex: 1; + + &:invalid, + &.invalid { + border-color: $justfix-orange; + } + + &:focus:invalid, + &.invalid:focus { + border-color: $justfix-black; + } + } +} + +.email-input-errors { + display: flex; + flex-direction: column; + + span { + display: flex; + align-items: center; + font-size: 1.3rem; + color: #ba4300; + margin-bottom: 0.5rem; + + svg { + margin-right: 0.75rem; + } + } +} diff --git a/client/src/styles/Login.scss b/client/src/styles/Login.scss index 329d42a90..1e62b75a4 100644 --- a/client/src/styles/Login.scss +++ b/client/src/styles/Login.scss @@ -10,37 +10,21 @@ flex-direction: column; margin-top: 1rem; - #input-field-error { - display: flex; - align-items: center; - font-size: 1.3rem; - color: #ba4300; - margin-bottom: 0.5rem; - - svg { - margin-right: 0.75rem; - } - } - - input[type="email"], - .password-input { + .email-input, + .password-input, + .user-type-container { margin-bottom: 2rem; } - input[type="submit"] { - padding: 1.6rem 3.2rem !important; - align-self: center; - margin: 0 auto !important; - } - - input:invalid, - .invalid { - border-color: $justfix-orange; - } + .submit-button-group { + display: flex; - input:focus:invalid, - .invalid:focus { - border-color: $justfix-black; + button[type="submit"], + input[type="submit"] { + padding: 1.6rem 3.2rem !important; + align-self: center; + margin: 0 auto !important; + } } } @@ -48,18 +32,6 @@ margin-top: 1.5rem; } - label { - @include desktop-eyebrow(); - // vars marked !important bc typography mixin is not available for inconsolata - font-weight: 600 !important; - font-family: $wow-font !important; - font-size: 1.3rem !important; - - text-align: left; - margin-top: 1rem; - margin-bottom: 0.5rem; - } - .building-page-footer { display: flex; flex-direction: column; @@ -113,6 +85,16 @@ margin-top: 0.5rem; } + .verify-email-container { + h4, + .resend-verify-label { + display: flex; + justify-content: center; + margin-bottom: 1.2rem; + margin-top: 1.4rem; + } + } + .jf-alert { width: auto; font-size: 1.3rem; diff --git a/client/src/styles/Password.scss b/client/src/styles/Password.scss index d42606e30..d7f2aecae 100644 --- a/client/src/styles/Password.scss +++ b/client/src/styles/Password.scss @@ -2,11 +2,23 @@ @import "_typography.scss"; .password-input-label { - font-weight: 600 !important; display: flex; align-items: center; justify-content: space-between; + label { + @include desktop-eyebrow(); + // vars marked !important bc typography mixin is not available for inconsolata + font-weight: 600 !important; + font-family: $wow-font !important; + font-size: 1.3rem !important; + color: $justfix-black; + + text-align: left; + margin-top: 1rem; + margin-bottom: 0.5rem; + } + a { color: $justfix-black; font-size: 1.3rem; @@ -14,13 +26,22 @@ } .password-input { - margin-bottom: 1rem; display: flex; input { flex: 1; border-top-right-radius: 0; border-bottom-right-radius: 0; + + &:invalid, + &.invalid { + border-color: $justfix-orange; + } + + &:focus:invalid, + &.invalid:focus { + border-color: $justfix-black; + } } button { diff --git a/client/src/styles/UserTypeInput.scss b/client/src/styles/UserTypeInput.scss new file mode 100644 index 000000000..69c3452b1 --- /dev/null +++ b/client/src/styles/UserTypeInput.scss @@ -0,0 +1,69 @@ +@import "_vars.scss"; +@import "_typography.scss"; + +.user-type-container { + .user-type-radio-group { + display: grid; + grid-template-columns: 2rem auto; + gap: 1rem 0.8rem; + align-items: center; + + label { + margin: 0; + } + + input[type="radio"] { + /* Remove most all native input styles */ + appearance: none; + /* For iOS < 15 */ + background-color: $justfix-white; + /* Not removed via appearance */ + margin: 0; + + font: inherit; + color: $justfix-black; + width: 2rem; + height: 2rem; + border: 0.15em solid $justfix-black; + border-radius: 50%; + transform: translateY(-0.075em); + + display: grid; + place-content: center; + + &::before { + content: ""; + width: 1.2rem; + height: 1.2rem; + border-radius: 50%; + transform: scale(0); + transition: 120ms transform ease-in-out; + box-shadow: inset 1em 1em $justfix-black; + /* Windows High Contrast Mode */ + background-color: CanvasText; + } + + &:checked::before { + transform: scale(1); + } + } + } + + .user-type-input-text-group { + display: flex; + flex-direction: column; + margin: 2.4rem 2.8rem 0 2.8rem; + + #input-field-error { + display: flex; + align-items: center; + font-size: 1.3rem; + color: #ba4300; + margin-bottom: 0.5rem; + + svg { + margin-right: 0.75rem; + } + } + } +} diff --git a/client/src/styles/_input.scss b/client/src/styles/_input.scss index 69b839e3c..d7158c621 100644 --- a/client/src/styles/_input.scss +++ b/client/src/styles/_input.scss @@ -3,7 +3,6 @@ @mixin input() { display: inline-block; border: 1px solid $justfix-black; - background: $justfix-white; padding: 1rem; font-size: inherit; border-radius: 4px; diff --git a/client/src/util/helpers.ts b/client/src/util/helpers.ts index 896bdc0f2..3a448b520 100644 --- a/client/src/util/helpers.ts +++ b/client/src/util/helpers.ts @@ -3,7 +3,7 @@ import { AddressRecord, HpdContactAddress, SearchAddressWithoutBbl } from "compo import { reportError } from "error-reporting"; import { t } from "@lingui/macro"; import { I18n, MessageDescriptor } from "@lingui/core"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, ChangeEvent } from "react"; import _ from "lodash"; const hpdComplaintTypeTranslations = new Map([ @@ -132,6 +132,24 @@ export function searchAddrsAreEqual( ); } +// https://www.codevertiser.com/reusable-input-component-react/ +export const useInput = (initialValue: string) => { + const [value, setValue] = useState(initialValue); + const [error, setError] = useState(false); + + const handleChange = (e: ChangeEvent) => { + setValue(e.target.value); + }; + + return { + value, + error, + onChange: handleChange, + setError, + setValue, + }; +}; + const helpers = { // filter repeated values in rbas and owners // uses Set which enforces uniqueness From cc1d308b8e134685a196e20f8ffd9bf6916aa1f6 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Mon, 19 Feb 2024 21:05:12 -0500 Subject: [PATCH 02/40] + --- client/src/components/Login.tsx | 514 +++++++++++++++++++++----------- 1 file changed, 342 insertions(+), 172 deletions(-) diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index 5dc15e8b7..35bb2ba5c 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -1,23 +1,31 @@ import React, { useState, useContext } from "react"; - -import "styles/Login.css"; -import "styles/_input.scss"; - +import { Trans, t } from "@lingui/macro"; import { I18n } from "@lingui/core"; import { withI18n } from "@lingui/react"; -import { Trans, t } from "@lingui/macro"; +import classNames from "classnames"; + +import AuthClient from "./AuthClient"; +import { JustfixUser } from "state-machine"; import { UserContext } from "./UserContext"; +import { useInput } from "util/helpers"; import PasswordInput from "./PasswordInput"; -import { JustfixUser } from "state-machine"; -import AuthClient from "./AuthClient"; +import EmailInput from "./EmailInput"; +import UserTypeInput from "./UserTypeInput"; import { Alert } from "./Alert"; -import { AlertIcon, InfoIcon } from "./Icons"; +import { InfoIcon } from "./Icons"; import Modal from "./Modal"; -export enum LoginState { - Default, +import "styles/Login.css"; +import "styles/UserTypeInput.css"; +import "styles/_input.scss"; + +enum Step { + CheckEmail, Login, - Register, + RegisterAccount, + RegisterUserType, + VerifyEmail, + VerifyEmailReminder, } type LoginProps = { @@ -25,22 +33,53 @@ type LoginProps = { onBuildingPage?: boolean; onSuccess?: (user: JustfixUser) => void; handleRedirect?: () => void; + registerInModal?: boolean; + setLoginRegisterInProgress?: React.Dispatch>; }; const LoginWithoutI18n = (props: LoginProps) => { - const { i18n, onBuildingPage, onSuccess, handleRedirect } = props; - const userContext = useContext(UserContext); + const { + i18n, + onBuildingPage, + onSuccess, + handleRedirect, + registerInModal, + setLoginRegisterInProgress, + } = props; - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [header, setHeader] = useState("Log in / sign up"); - const [subheader, setSubheader] = useState(""); - const [loginState, setLoginState] = useState(LoginState.Default); - const isDefaultState = loginState === LoginState.Default; - const isRegisterState = loginState === LoginState.Register; + const userContext = useContext(UserContext); + const [showRegisterModal, setShowRegisterModal] = useState(false); const [showInfoModal, setShowInfoModal] = useState(false); - const [emailFormatError, setEmailFormatError] = useState(false); + + const [step, setStep] = useState(Step.CheckEmail); + const isCheckEmailStep = step === Step.CheckEmail; + const isLoginStep = step === Step.Login; + const isRegisterAccountStep = step === Step.RegisterAccount; + const isRegisterUserTypeStep = step === Step.RegisterUserType; + const isVerifyEmailStep = step === Step.VerifyEmail; + const isVerifyEmailReminderStep = step === Step.VerifyEmailReminder; + + const { + value: email, + error: emailError, + setError: setEmailError, + onChange: onChangeEmail, + } = useInput(""); + const { + value: password, + error: passwordError, + setError: setPasswordError, + onChange: onChangePassword, + } = useInput(""); + const { + value: userType, + error: userTypeError, + setError: setUserTypeError, + setValue: setUserType, + onChange: onChangeUserType, + } = useInput(""); + const [emptyAuthError, setEmptyAuthError] = useState(false); const [invalidAuthError, setInvalidAuthError] = useState(false); const [existingUserError, setExistingUserError] = useState(false); @@ -51,70 +90,6 @@ const LoginWithoutI18n = (props: LoginProps) => { setExistingUserError(false); }; - const toggleLoginState = (endState: LoginState) => { - if (endState === LoginState.Register) { - setLoginState(LoginState.Register); - if (!onBuildingPage) { - setHeader("Sign up"); - setSubheader("With an account you can save buildings and get weekly updates"); - } - } else if (endState === LoginState.Login) { - setLoginState(LoginState.Login); - if (!onBuildingPage) { - setHeader("Log in"); - setSubheader(""); - } - } - }; - - const handleEmailSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (isDefaultState && !!username && !emailFormatError) { - const existingUser = await AuthClient.isEmailAlreadyUsed(username); - - if (existingUser) { - setExistingUserError(true); - toggleLoginState(LoginState.Login); - } else { - toggleLoginState(LoginState.Register); - } - } - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - resetErrorStates(); - - if (!username || !password) { - setEmptyAuthError(true); - return; - } - - const existingUser = await AuthClient.isEmailAlreadyUsed(username); - if (existingUser) { - if (isRegisterState) { - setExistingUserError(true); - } else { - const error = await userContext.login(username, password, onSuccess); - if (!!error) { - setInvalidAuthError(true); - } else { - handleRedirect && handleRedirect(); - } - } - } else { - const error = isRegisterState - ? await userContext.register(username, password, onSuccess) - : await userContext.login(username, password, onSuccess); - - if (!!error) { - setInvalidAuthError(true); - } else { - handleRedirect && handleRedirect(); - } - } - }; - const renderPageLevelAlert = ( type: "error" | "success" | "info", message: string, @@ -130,10 +105,7 @@ const LoginWithoutI18n = (props: LoginProps) => { > {message} {showLogin && ( - )} @@ -152,10 +124,10 @@ const LoginWithoutI18n = (props: LoginProps) => { alertMessage = i18n._(t`The email and/or password cannot be blank.`); return renderPageLevelAlert("error", alertMessage); case existingUserError: - if (isRegisterState) { + if (isRegisterAccountStep) { alertMessage = i18n._(t`That email is already used.`); // show login button in alert - return renderPageLevelAlert("error", alertMessage, !onBuildingPage); + return renderPageLevelAlert("error", alertMessage, !onBuildingPage || showRegisterModal); } else if (onBuildingPage) { alertMessage = i18n._(t`Your email is associated with an account. Log in below.`); return renderPageLevelAlert("info", alertMessage); @@ -163,15 +135,6 @@ const LoginWithoutI18n = (props: LoginProps) => { } }; - const renderHeader = () => { - return ( - <> -

{i18n._(t`${header}`)}

-
{i18n._(t`${subheader}`)}
- - ); - }; - const renderFooter = () => { return (
@@ -186,13 +149,10 @@ const LoginWithoutI18n = (props: LoginProps) => {
- {isRegisterState ? ( + {isRegisterAccountStep ? ( <> Already have an account? - @@ -201,7 +161,7 @@ const LoginWithoutI18n = (props: LoginProps) => { Don't have an account? @@ -231,79 +191,289 @@ const LoginWithoutI18n = (props: LoginProps) => { ); }; - const isBadEmailFormat = () => { - /* valid email regex rules - alpha numeric characters are ok, upper/lower case agnostic - username: leading \_ ok, chars \_\.\-\+ ok in all other positions - domain name: chars \.\- ok as long as not leading. must end in a \. and at least two alphabet chars */ - const pattern = - "^([a-zA-Z0-9_]+[a-zA-Z0-9+_.-]+@[a-zA-Z0-9]+[a-zA-Z0-9.-]+[a-zA-Z0-9]+.[a-zA-Z]{2,})$"; - const input = document.getElementById("email-input") as HTMLElement; - const inputValue = (input as HTMLInputElement).value; - - // HTML input element has loose email validation requirements, so we check the input against a custom regex - const passStrictRegex = inputValue.match(pattern); - const passAutoValidation = document.querySelectorAll("input:invalid").length === 0; - - if (!passAutoValidation || !passStrictRegex) { - setEmailFormatError(true); - input.className = input.className + " invalid"; - } else { - setEmailFormatError(false); - input.className = input.className.split(" ")[0]; + const renderVerifyEmail = () => { + return ( +
+

+

+ {i18n._( + t`We just sent an email verification link to ${email}. To complete signup, please click the link in your email.` + )} +

+ + Didn’t receive the link? + + +
+ ); + }; + + const renderVerifyEmailReminder = () => { + return ( + <> + Verify your email to start receiving updates + {i18n._(t`Click the link we sent to ${email}. It may take a few minutes to arrive.`)} +
+
+ Once your email has been verified, you’ll be signed up for Data Updates. +
+
+ + + ); + }; + + const onEmailSubmit = async () => { + !!setLoginRegisterInProgress && setLoginRegisterInProgress(true); + + if (!email) { + // TODO: Make this specific to email only + // TODO: raise this error if showRegisterModal? + showRegisterModal && setEmptyAuthError(true); + registerInModal && setShowRegisterModal(true); + return; + } + + if (!!email && emailError) { + // TODO Raise a new kind of bad email error, in addition to the input level one? + // in chrome the browser adds an alert on the textbox explaining the format error + // TODO: open modal if email error? + if (registerInModal && !showRegisterModal) { + setShowRegisterModal(true); + } + return; + } + + if (!!email && !emailError) { + const existingUser = await AuthClient.isEmailAlreadyUsed(email); + + if (existingUser) { + setExistingUserError(true); + setStep(Step.Login); + } else { + setStep(Step.RegisterAccount); + if (registerInModal && !showRegisterModal) { + setShowRegisterModal(true); + } + } } }; - return ( -
- {renderAlert()} - {!onBuildingPage && renderHeader()} -
- Email address - {emailFormatError && ( - - - Please enter a valid email address. - + const onLoginSubmit = async () => { + resetErrorStates(); + + if (!email || !password) { + setEmptyAuthError(true); + return; + } + + // context doesn't update immediately so can't check verified status within this onSubmit without returning the user + const resp = await userContext.login(email, password, onSuccess); + + if (!!resp?.error) { + setInvalidAuthError(true); + return; + } + + if (!!resp?.user && !resp.user.verified) { + setStep(Step.VerifyEmailReminder); + registerInModal && setShowRegisterModal(true); + return; + } + + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); + + handleRedirect && handleRedirect(); + }; + + const onAccountSubmit = async () => { + if (!email) { + setEmptyAuthError(true); + return; + } + + if (!!email && emailError) { + // TODO Raise a new kind of bad email error, in addition to the input level one? + // in chrome the browser adds an alert on the textbox explaining the format error + return; + } + + const existingUser = await AuthClient.isEmailAlreadyUsed(email); + if (existingUser) { + setExistingUserError(true); + return; + } + + if (!password) { + setEmptyAuthError(true); + return; + } + + if (!!password && passwordError) { + // TODO: raise alert here, or ok with input level invalid pw note? + return; + } + + setStep(Step.RegisterUserType); + }; + + const onUserTypeSubmit = async () => { + if (!userType || userTypeError) { + // TODO: raise alert here that this is required? + return; + } + + const resp = await userContext.register(email, password, onSuccess); + + if (!!resp?.error) { + setInvalidAuthError(true); + return; + } + + if (!registerInModal) { + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); + } + + setStep(Step.VerifyEmail); + + // TODO: how to redirect when there is verify step? + // handleRedirect && handleRedirect(); + }; + + let headerText = ""; + let onSubmit = () => {}; + let submitButtonText = ""; + switch (step) { + case Step.CheckEmail: + headerText = onBuildingPage + ? i18n._(t`Get weekly Data Updates for complaints, violations, and evictions.`) + : i18n._(t`Log in / sign up`); + onSubmit = onEmailSubmit; + submitButtonText = + !onBuildingPage || showRegisterModal ? i18n._(t`Submit`) : i18n._(t`Get updates`); + break; + case Step.Login: + headerText = onBuildingPage ? "" : i18n._(t`Log in`); + onSubmit = onLoginSubmit; + submitButtonText = i18n._(t`Log in`); + break; + case Step.RegisterAccount: + headerText = onBuildingPage + ? i18n._(t`Create a password to start receiving Data Updates`) + : i18n._(t`Sign up`); + onSubmit = onAccountSubmit; + submitButtonText = i18n._(t`Next`); + break; + case Step.RegisterUserType: + headerText = i18n._(t`Which best describes you?`); + onSubmit = onUserTypeSubmit; + submitButtonText = "Sign up"; + break; + } + + const renderLoginFlow = () => { + return ( +
+ {!isVerifyEmailStep && !isVerifyEmailReminderStep && ( + <> + {(!onBuildingPage || showRegisterModal) && ( +

{headerText}

+ )} + {renderAlert()} + { + e.preventDefault(); + resetErrorStates(); + onSubmit(); + }} + > + {(isCheckEmailStep || isLoginStep || isRegisterAccountStep) && ( + + )} + {(isLoginStep || isRegisterAccountStep) && ( + + )} + {isRegisterUserTypeStep && ( + + )} +
+ {/* {isRegisterUserTypeStep && ( + + )} */} + +
+ + )} - { - setUsername(e.target.value); + {isVerifyEmailStep && renderVerifyEmail()} + {isVerifyEmailReminderStep && renderVerifyEmailReminder()} + {/* {onBuildingPage && renderFooter()} */} +
+ ); + }; + + return ( + <> + {(!registerInModal || isCheckEmailStep || isLoginStep || isVerifyEmailStep) && + renderLoginFlow()} + {registerInModal && ( + { + resetErrorStates(); + setShowRegisterModal(false); + if (isVerifyEmailStep || isVerifyEmailReminderStep) { + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); + } }} - onBlur={isBadEmailFormat} - value={username} - // note: required={true} removed bc any empty state registers as invalid state - /> - {!isDefaultState && ( - - )} - - - {onBuildingPage && renderFooter()} -
+ > + {renderLoginFlow()} + + )} + ); }; -const Login = withI18n()(LoginWithoutI18n); +export const Login = withI18n()(LoginWithoutI18n); export default Login; From dd08c25b89925fe7a96910f33a150723678ac782 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Mon, 19 Feb 2024 21:12:05 -0500 Subject: [PATCH 03/40] ignore unused footer for now --- client/src/components/Login.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index 35bb2ba5c..0548e83dd 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -135,6 +135,8 @@ const LoginWithoutI18n = (props: LoginProps) => { } }; + // TODO: do we need this anymore? + // eslint-disable-next-line @typescript-eslint/no-unused-vars const renderFooter = () => { return (
From 0beb6b3f4c47f0d4c992a92e7c4cbd8927a59faa Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Tue, 20 Feb 2024 12:41:56 -0500 Subject: [PATCH 04/40] add preview urls pattern to cors whitelist --- project/settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project/settings.py b/project/settings.py index 134985931..8180c17a7 100644 --- a/project/settings.py +++ b/project/settings.py @@ -91,12 +91,12 @@ def get_required_env(key: str) -> str: "http://127.0.0.1:3000", "http://localhost:3000", "http://demo-wowserver.justfix.org", - "https://deploy-preview-703--wow-django-dev.netlify.app", - "https://deploy-preview-785--wow-django-dev.netlify.app", - "https://deploy-preview-811--wow-django-dev.netlify.app", - "https://deploy-preview-843--wow-django-dev.netlify.app", ) +CORS_ALLOWED_ORIGIN_REGEXES = [ + r"https://\w+\.deploy-preview-(?:\d{1,4})--wow-django-dev\.netlify\.app" +] + # This is based off the default Django logging configuration: # https://github.com/django/django/blob/master/django/utils/log.py LOGGING = { From 8343e22810255a1f67a26e5be1084a05a2af243b Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Tue, 20 Feb 2024 13:54:14 -0500 Subject: [PATCH 05/40] add back preview urls to whitelist --- project/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/project/settings.py b/project/settings.py index 8180c17a7..9cf5cbc80 100644 --- a/project/settings.py +++ b/project/settings.py @@ -91,6 +91,11 @@ def get_required_env(key: str) -> str: "http://127.0.0.1:3000", "http://localhost:3000", "http://demo-wowserver.justfix.org", + "https://deploy-preview-703--wow-django-dev.netlify.app", + "https://deploy-preview-785--wow-django-dev.netlify.app", + "https://deploy-preview-811--wow-django-dev.netlify.app", + "https://deploy-preview-843--wow-django-dev.netlify.app", + "https://deploy-preview-849--wow-django-dev.netlify.app", ) CORS_ALLOWED_ORIGIN_REGEXES = [ From 7c9eb334f36d7188fe59db4e1cd9d824a3b84236 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Tue, 20 Feb 2024 19:45:55 -0500 Subject: [PATCH 06/40] clean up styles for login - text styles, padding, etc. --- client/src/components/EmailAlertSignup.tsx | 5 +- client/src/components/EmailInput.tsx | 5 +- client/src/components/PasswordInput.tsx | 6 +- client/src/styles/EmailAlertSignup.scss | 11 +- client/src/styles/EmailInput.scss | 92 ++++++------- client/src/styles/Login.scss | 8 +- client/src/styles/Password.scss | 149 +++++++++++---------- client/src/styles/_button.scss | 1 + 8 files changed, 140 insertions(+), 137 deletions(-) diff --git a/client/src/components/EmailAlertSignup.tsx b/client/src/components/EmailAlertSignup.tsx index 1f62d7663..347aeecb0 100644 --- a/client/src/components/EmailAlertSignup.tsx +++ b/client/src/components/EmailAlertSignup.tsx @@ -98,10 +98,7 @@ const EmailAlertSignupWithoutI18n = (props: EmailAlertProps) => {
{({ i18n }) => ( -
+
diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index 0548e83dd..e9c458783 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -63,7 +63,9 @@ const LoginWithoutI18n = (props: LoginProps) => { const { value: email, error: emailError, + showError: showEmailError, setError: setEmailError, + setShowError: setShowEmailError, onChange: onChangeEmail, } = useInput(""); const { @@ -80,12 +82,10 @@ const LoginWithoutI18n = (props: LoginProps) => { onChange: onChangeUserType, } = useInput(""); - const [emptyAuthError, setEmptyAuthError] = useState(false); const [invalidAuthError, setInvalidAuthError] = useState(false); const [existingUserError, setExistingUserError] = useState(false); const resetErrorStates = () => { - setEmptyAuthError(false); setInvalidAuthError(false); setExistingUserError(false); }; @@ -120,23 +120,13 @@ const LoginWithoutI18n = (props: LoginProps) => { case invalidAuthError: alertMessage = i18n._(t`The email and/or password you entered is incorrect.`); return renderPageLevelAlert("error", alertMessage); - case emptyAuthError: - alertMessage = i18n._(t`The email and/or password cannot be blank.`); - return renderPageLevelAlert("error", alertMessage); - case existingUserError: - if (isRegisterAccountStep) { - alertMessage = i18n._(t`That email is already used.`); - // show login button in alert - return renderPageLevelAlert("error", alertMessage, !onBuildingPage || showRegisterModal); - } else if (onBuildingPage) { - alertMessage = i18n._(t`Your email is associated with an account. Log in below.`); - return renderPageLevelAlert("info", alertMessage); - } + case existingUserError && isRegisterAccountStep: + alertMessage = i18n._(t`That email is already used.`); + // show login button in alert + return renderPageLevelAlert("error", alertMessage, !onBuildingPage || showRegisterModal); } }; - // TODO: do we need this anymore? - // eslint-disable-next-line @typescript-eslint/no-unused-vars const renderFooter = () => { return (
@@ -239,20 +229,17 @@ const LoginWithoutI18n = (props: LoginProps) => { !!setLoginRegisterInProgress && setLoginRegisterInProgress(true); if (!email) { - // TODO: Make this specific to email only - // TODO: raise this error if showRegisterModal? - showRegisterModal && setEmptyAuthError(true); + if (!onBuildingPage || showRegisterModal) { + setEmailError(true); + setShowEmailError(true); + } registerInModal && setShowRegisterModal(true); return; } if (!!email && emailError) { - // TODO Raise a new kind of bad email error, in addition to the input level one? - // in chrome the browser adds an alert on the textbox explaining the format error - // TODO: open modal if email error? - if (registerInModal && !showRegisterModal) { - setShowRegisterModal(true); - } + setEmailError(true); + setShowEmailError(true); return; } @@ -260,7 +247,6 @@ const LoginWithoutI18n = (props: LoginProps) => { const existingUser = await AuthClient.isEmailAlreadyUsed(email); if (existingUser) { - setExistingUserError(true); setStep(Step.Login); } else { setStep(Step.RegisterAccount); @@ -274,12 +260,11 @@ const LoginWithoutI18n = (props: LoginProps) => { const onLoginSubmit = async () => { resetErrorStates(); - if (!email || !password) { - setEmptyAuthError(true); + if (!email || emailError || !password || passwordError) { return; } - // context doesn't update immediately so can't check verified status within this onSubmit without returning the user + // context doesn't update immediately so need to reurn user to check verified status const resp = await userContext.login(email, password, onSuccess); if (!!resp?.error) { @@ -287,26 +272,21 @@ const LoginWithoutI18n = (props: LoginProps) => { return; } + if (!onBuildingPage) { + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); + handleRedirect && handleRedirect(); + return; + } + if (!!resp?.user && !resp.user.verified) { setStep(Step.VerifyEmailReminder); registerInModal && setShowRegisterModal(true); return; } - - !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); - - handleRedirect && handleRedirect(); }; const onAccountSubmit = async () => { - if (!email) { - setEmptyAuthError(true); - return; - } - - if (!!email && emailError) { - // TODO Raise a new kind of bad email error, in addition to the input level one? - // in chrome the browser adds an alert on the textbox explaining the format error + if (!email || emailError) { return; } @@ -316,13 +296,7 @@ const LoginWithoutI18n = (props: LoginProps) => { return; } - if (!password) { - setEmptyAuthError(true); - return; - } - - if (!!password && passwordError) { - // TODO: raise alert here, or ok with input level invalid pw note? + if (!password || passwordError) { return; } @@ -332,6 +306,7 @@ const LoginWithoutI18n = (props: LoginProps) => { const onUserTypeSubmit = async () => { if (!userType || userTypeError) { // TODO: raise alert here that this is required? + setUserTypeError(true); return; } @@ -342,14 +317,17 @@ const LoginWithoutI18n = (props: LoginProps) => { return; } + if (!onBuildingPage) { + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); + handleRedirect && handleRedirect(); + return; + } + if (!registerInModal) { !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); } setStep(Step.VerifyEmail); - - // TODO: how to redirect when there is verify step? - // handleRedirect && handleRedirect(); }; let headerText = ""; @@ -406,6 +384,8 @@ const LoginWithoutI18n = (props: LoginProps) => { onChange={onChangeEmail} error={emailError} setError={setEmailError} + showError={showEmailError} + // note: required={true} removed bc any empty state registers as invalid state /> )} {(isLoginStep || isRegisterAccountStep) && ( @@ -447,14 +427,14 @@ const LoginWithoutI18n = (props: LoginProps) => { )} {isVerifyEmailStep && renderVerifyEmail()} {isVerifyEmailReminderStep && renderVerifyEmailReminder()} - {/* {onBuildingPage && renderFooter()} */} + {isRegisterAccountStep && renderFooter()}
); }; return ( <> - {(!registerInModal || isCheckEmailStep || isLoginStep || isVerifyEmailStep) && + {(!showRegisterModal || isCheckEmailStep || isLoginStep || isVerifyEmailStep) && renderLoginFlow()} {registerInModal && ( { width={40} onClose={() => { resetErrorStates(); + setShowEmailError(false); setShowRegisterModal(false); - if (isVerifyEmailStep || isVerifyEmailReminderStep) { - !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); - } + setStep(Step.CheckEmail); + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); }} > {renderLoginFlow()} diff --git a/client/src/components/UserTypeInput.tsx b/client/src/components/UserTypeInput.tsx index 96066cc13..dda5e3c76 100644 --- a/client/src/components/UserTypeInput.tsx +++ b/client/src/components/UserTypeInput.tsx @@ -3,6 +3,7 @@ import { Trans, t } from "@lingui/macro"; import { I18n } from "@lingui/core"; import { withI18n } from "@lingui/react"; import { AlertIcon } from "./Icons"; +import classNames from "classnames"; type UserTypeInputProps = { i18n: I18n; @@ -31,15 +32,10 @@ const UserTypeInputWithoutI18n = (props: UserTypeInputProps) => { }; const missingOtherText = () => { - const input = document.getElementById("user-type-input-other-text") as HTMLElement; - const inputValue = (input as HTMLInputElement).value; - - if (!inputValue) { + if (!userType) { setError(true); - input.className = input.className + " invalid"; } else { setError(false); - input.className = input.className.split(" ")[0]; } }; @@ -54,6 +50,12 @@ const UserTypeInputWithoutI18n = (props: UserTypeInputProps) => { return (
+ {error && activeRadio !== USER_TYPES.other && ( + + + Please select an option. + + )}
{ required={required} autoFocus id="user-type-input-other-text" - className="input" + className={classNames("input", { invalid: error })} type="text" name="user-type" value={userType} diff --git a/client/src/styles/EmailInput.scss b/client/src/styles/EmailInput.scss index 36c365d7f..55786b88e 100644 --- a/client/src/styles/EmailInput.scss +++ b/client/src/styles/EmailInput.scss @@ -32,12 +32,10 @@ input { flex: 1; - &:invalid, &.invalid { border-color: $justfix-orange; } - &:focus:invalid, &.invalid:focus { border-color: $justfix-black; } diff --git a/client/src/styles/Login.scss b/client/src/styles/Login.scss index 4a84ba809..f142167b7 100644 --- a/client/src/styles/Login.scss +++ b/client/src/styles/Login.scss @@ -5,6 +5,10 @@ .Login, .ForgotPasswordPage, .ResetPasswordPage { + display: flex; + flex-direction: column; + gap: 2.4rem; + .input-group { display: flex; flex-direction: column; @@ -70,6 +74,7 @@ h4 { font-size: 2.4rem; + margin-bottom: 0; } h5 { font-size: 1.6rem; diff --git a/client/src/styles/UserTypeInput.scss b/client/src/styles/UserTypeInput.scss index 69c3452b1..9fdd636dc 100644 --- a/client/src/styles/UserTypeInput.scss +++ b/client/src/styles/UserTypeInput.scss @@ -54,16 +54,26 @@ flex-direction: column; margin: 2.4rem 2.8rem 0 2.8rem; - #input-field-error { - display: flex; - align-items: center; - font-size: 1.3rem; - color: #ba4300; - margin-bottom: 0.5rem; + input { + &.invalid { + border-color: $justfix-orange; + } - svg { - margin-right: 0.75rem; + &.invalid:focus { + border-color: $justfix-black; } } } + + #input-field-error { + display: flex; + align-items: center; + font-size: 1.3rem; + color: #ba4300; + margin-bottom: 0.5rem; + + svg { + margin-right: 0.75rem; + } + } } diff --git a/client/src/util/helpers.ts b/client/src/util/helpers.ts index 3a448b520..be38f0cd5 100644 --- a/client/src/util/helpers.ts +++ b/client/src/util/helpers.ts @@ -136,6 +136,7 @@ export function searchAddrsAreEqual( export const useInput = (initialValue: string) => { const [value, setValue] = useState(initialValue); const [error, setError] = useState(false); + const [showError, setShowError] = useState(false); const handleChange = (e: ChangeEvent) => { setValue(e.target.value); @@ -144,9 +145,11 @@ export const useInput = (initialValue: string) => { return { value, error, - onChange: handleChange, - setError, + showError, setValue, + setError, + setShowError, + onChange: handleChange, }; }; From 64768cab2a8dc0476cc62930f8819384cabd1028 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Wed, 21 Feb 2024 15:40:55 -0500 Subject: [PATCH 08/40] login flow ending --- client/src/components/Login.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index e9c458783..1c8505ad8 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -272,17 +272,12 @@ const LoginWithoutI18n = (props: LoginProps) => { return; } + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); + if (!onBuildingPage) { - !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); handleRedirect && handleRedirect(); return; } - - if (!!resp?.user && !resp.user.verified) { - setStep(Step.VerifyEmailReminder); - registerInModal && setShowRegisterModal(true); - return; - } }; const onAccountSubmit = async () => { @@ -390,7 +385,7 @@ const LoginWithoutI18n = (props: LoginProps) => { )} {(isLoginStep || isRegisterAccountStep) && ( Date: Wed, 21 Feb 2024 15:41:07 -0500 Subject: [PATCH 09/40] ix double padding on subscribed box --- client/src/components/EmailAlertSignup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/EmailAlertSignup.tsx b/client/src/components/EmailAlertSignup.tsx index 347aeecb0..ac00c9304 100644 --- a/client/src/components/EmailAlertSignup.tsx +++ b/client/src/components/EmailAlertSignup.tsx @@ -63,7 +63,7 @@ const BuildingSubscribeWithoutI18n = (props: BuildingSubscribeProps) => { return ( {({ i18n }) => ( -
+
{!(subscriptions && !!subscriptions?.find((s) => s.bbl === bbl)) ? ( +
+ + Email verification required +
+
+ {i18n._(t`Click the link we sent to ${email}. It may take a few minutes to arrive.`)}
+
); }; diff --git a/client/src/components/Icons.tsx b/client/src/components/Icons.tsx index 8d625325b..c2ae08635 100644 --- a/client/src/components/Icons.tsx +++ b/client/src/components/Icons.tsx @@ -33,15 +33,15 @@ export const CheckIcon = (props: SVGProps) => ( export const SubscribedIcon = (props: SVGProps) => ( - - + + ); diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index b4f496c86..f3ef8a4d3 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -338,7 +338,9 @@ const LoginWithoutI18n = (props: LoginProps) => { !onBuildingPage || showRegisterModal ? i18n._(t`Submit`) : i18n._(t`Get updates`); break; case Step.Login: - headerText = onBuildingPage ? "" : i18n._(t`Log in`); + headerText = onBuildingPage + ? i18n._(t`Get weekly Data Updates for complaints, violations, and evictions.`) + : i18n._(t`Log in`); onSubmit = onLoginSubmit; submitButtonText = i18n._(t`Log in`); break; @@ -410,7 +412,7 @@ const LoginWithoutI18n = (props: LoginProps) => { className="button is-primary button-back" onClick={() => setLoginStep(LoginStep.RegisterAccount)} > - Back + Back )} */}
diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index f3ef8a4d3..bc0cb5716 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -120,10 +120,15 @@ const LoginWithoutI18n = (props: LoginProps) => { case invalidAuthError: alertMessage = i18n._(t`The email and/or password you entered is incorrect.`); return renderPageLevelAlert("error", alertMessage); - case existingUserError && isRegisterAccountStep: - alertMessage = i18n._(t`That email is already used.`); - // show login button in alert - return renderPageLevelAlert("error", alertMessage, !onBuildingPage || showRegisterModal); + case existingUserError: + if (isRegisterAccountStep) { + alertMessage = i18n._(t`That email is already used.`); + // show login button in alert + return renderPageLevelAlert("error", alertMessage, !onBuildingPage || showRegisterModal); + } else if (isLoginStep && !(onBuildingPage && !showRegisterModal)) { + alertMessage = i18n._(t`Your email is associated with an account. Log in below.`); + return renderPageLevelAlert("info", alertMessage); + } } }; @@ -248,6 +253,7 @@ const LoginWithoutI18n = (props: LoginProps) => { if (existingUser) { setStep(Step.Login); + setExistingUserError(true); } else { setStep(Step.RegisterAccount); if (registerInModal && !showRegisterModal) { @@ -287,6 +293,7 @@ const LoginWithoutI18n = (props: LoginProps) => { const existingUser = await AuthClient.isEmailAlreadyUsed(email); if (existingUser) { + setStep(Step.Login); setExistingUserError(true); return; } @@ -338,9 +345,11 @@ const LoginWithoutI18n = (props: LoginProps) => { !onBuildingPage || showRegisterModal ? i18n._(t`Submit`) : i18n._(t`Get updates`); break; case Step.Login: - headerText = onBuildingPage + headerText = !onBuildingPage + ? i18n._(t`Log in`) + : showRegisterModal ? i18n._(t`Get weekly Data Updates for complaints, violations, and evictions.`) - : i18n._(t`Log in`); + : i18n._(t`Log in to start getting updates for this building.`); onSubmit = onLoginSubmit; submitButtonText = i18n._(t`Log in`); break; @@ -366,6 +375,9 @@ const LoginWithoutI18n = (props: LoginProps) => { {(!onBuildingPage || showRegisterModal) && (

{headerText}

)} + {onBuildingPage && !showRegisterModal && ( +
{headerText}
+ )} {renderAlert()}
Date: Thu, 22 Feb 2024 18:32:30 -0500 Subject: [PATCH 13/40] update verify copy/styles, fix input-level errors for sign up --- client/src/components/EmailAlertSignup.tsx | 8 +- client/src/components/Login.tsx | 134 +++++++++++---------- client/src/components/PasswordInput.tsx | 9 +- client/src/styles/Login.scss | 13 +- 4 files changed, 94 insertions(+), 70 deletions(-) diff --git a/client/src/components/EmailAlertSignup.tsx b/client/src/components/EmailAlertSignup.tsx index 3a8eff9a4..a08d47b54 100644 --- a/client/src/components/EmailAlertSignup.tsx +++ b/client/src/components/EmailAlertSignup.tsx @@ -10,6 +10,7 @@ import "styles/EmailAlertSignup.css"; import { JustfixUser } from "state-machine"; import AuthClient from "./AuthClient"; import { AlertIconOutline, SubscribedIcon } from "./Icons"; +import { Alert } from "./Alert"; type BuildingSubscribeProps = withI18nProps & { bbl: string; @@ -42,10 +43,9 @@ const BuildingSubscribeWithoutI18n = (props: BuildingSubscribeProps) => { const showEmailVerification = (i18n: any) => { return (
-
- - Email verification required -
+ + Verify your email to start receiving updates. +
{i18n._(t`Click the link we sent to ${email}. It may take a few minutes to arrive.`)}
diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index bc0cb5716..d0658019f 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -71,7 +71,9 @@ const LoginWithoutI18n = (props: LoginProps) => { const { value: password, error: passwordError, + showError: showPasswordError, setError: setPasswordError, + setShowError: setShowPasswordError, onChange: onChangePassword, } = useInput(""); const { @@ -125,7 +127,7 @@ const LoginWithoutI18n = (props: LoginProps) => { alertMessage = i18n._(t`That email is already used.`); // show login button in alert return renderPageLevelAlert("error", alertMessage, !onBuildingPage || showRegisterModal); - } else if (isLoginStep && !(onBuildingPage && !showRegisterModal)) { + } else if (isLoginStep && onBuildingPage && showRegisterModal) { alertMessage = i18n._(t`Your email is associated with an account. Log in below.`); return renderPageLevelAlert("info", alertMessage); } @@ -191,14 +193,9 @@ const LoginWithoutI18n = (props: LoginProps) => { const renderVerifyEmail = () => { return (
-

-

- {i18n._( - t`We just sent an email verification link to ${email}. To complete signup, please click the link in your email.` - )} -

+

{i18n._(t`Click the link we sent to ${email}. It may take a few minutes to arrive.`)}

- Didn’t receive the link? + Didn’t get the link? )} */} - -
- - + +
+ )} {isVerifyEmailStep && renderVerifyEmail()} {isVerifyEmailReminderStep && renderVerifyEmailReminder()} diff --git a/client/src/components/PasswordInput.tsx b/client/src/components/PasswordInput.tsx index dd2d6fc5e..91587a126 100644 --- a/client/src/components/PasswordInput.tsx +++ b/client/src/components/PasswordInput.tsx @@ -10,6 +10,7 @@ import { createWhoOwnsWhatRoutePaths } from "routes"; import { I18n } from "@lingui/core"; import { t } from "@lingui/macro"; import { HideIcon, ShowIcon } from "./Icons"; +import classNames from "classnames"; type PasswordRule = { regex: RegExp; @@ -34,8 +35,10 @@ type PasswordInputProps = { i18n: I18n; labelText: string; password: string; - username?: string; onChange: (e: ChangeEvent) => void; + error: boolean; + showError: boolean; + username?: string; setError?: React.Dispatch>; showPasswordRules?: boolean; showForgotPassword?: boolean; @@ -48,7 +51,9 @@ const PasswordInputWithoutI18n = (props: PasswordInputProps) => { labelText, username, password, + error, setError, + showError, onChange, showPasswordRules, showForgotPassword, @@ -88,7 +93,7 @@ const PasswordInputWithoutI18n = (props: PasswordInputProps) => { Date: Fri, 23 Feb 2024 14:37:12 -0500 Subject: [PATCH 14/40] update password props on reset and change pages --- client/src/components/UserSettingField.tsx | 31 +++++++++++---------- client/src/containers/ResetPasswordPage.tsx | 9 +++++- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/client/src/components/UserSettingField.tsx b/client/src/components/UserSettingField.tsx index be92e4627..199f37ce2 100644 --- a/client/src/components/UserSettingField.tsx +++ b/client/src/components/UserSettingField.tsx @@ -13,8 +13,18 @@ type PasswordSettingFieldProps = withI18nProps & { const PasswordSettingFieldWithoutI18n = (props: PasswordSettingFieldProps) => { const { i18n, onSubmit } = props; - const { value: currentPassword, onChange: onChangeCurrentPassword } = useInput(""); - const { value: newPassword, onChange: onChangeNewPassword } = useInput(""); + const { + value: currentPassword, + error: currentPasswordError, + showError: showCurrentPasswordError, + onChange: onChangeCurrentPassword, + } = useInput(""); + const { + value: newPassword, + error: newPasswordError, + showError: showNewPasswordError, + onChange: onChangeNewPassword, + } = useInput(""); // const [showCurrentPassword, setShowCurrentPassword] = useState(false); const handleSubmit = () => { @@ -23,29 +33,20 @@ const PasswordSettingFieldWithoutI18n = (props: PasswordSettingFieldProps) => { return ( - Password - - {/*
- - -
*/}
); diff --git a/client/src/containers/ResetPasswordPage.tsx b/client/src/containers/ResetPasswordPage.tsx index d84d46714..4147df34b 100644 --- a/client/src/containers/ResetPasswordPage.tsx +++ b/client/src/containers/ResetPasswordPage.tsx @@ -17,7 +17,12 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { const [requestSent, setRequestSent] = React.useState(false); const userContext = useContext(UserContext); - const { value: password, onChange: onChangePassword } = useInput(""); + const { + value: password, + error: passwordError, + showError: showPasswordError, + onChange: onChangePassword, + } = useInput(""); const delaySeconds = 5; const baseUrl = window.location.origin; @@ -56,6 +61,8 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { showPasswordRules={true} password={password} onChange={onChangePassword} + error={passwordError} + showError={showPasswordError} />
- )} -
- -
-
- ); -}; + ); + } +); PasswordInputWithoutI18n.defaultProps = { showPasswordRules: false, diff --git a/client/src/components/UserSettingField.tsx b/client/src/components/UserSettingField.tsx index 049a9899f..bd29cd0a2 100644 --- a/client/src/components/UserSettingField.tsx +++ b/client/src/components/UserSettingField.tsx @@ -42,7 +42,7 @@ const PasswordSettingFieldWithoutI18n = (props: PasswordSettingFieldProps) => { showError={showCurrentPasswordError} setError={setCurrentPasswordError} onChange={onChangeCurrentPassword} - inputId="old-password-input" + id="old-password-input" /> { showError={showNewPasswordError} setError={setNewPasswordError} onChange={onChangeNewPassword} - inputId="new-password-input" + id="new-password-input" /> ); From fff9d57fd453a694638f6e8617cb43e3b0740e09 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Mon, 26 Feb 2024 16:34:12 -0500 Subject: [PATCH 22/40] remove old id prop for pw input --- client/src/components/PasswordInput.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/PasswordInput.tsx b/client/src/components/PasswordInput.tsx index 7810e1268..310e6d8c8 100644 --- a/client/src/components/PasswordInput.tsx +++ b/client/src/components/PasswordInput.tsx @@ -42,7 +42,6 @@ interface PasswordInputProps extends React.ComponentPropsWithoutRef<"input"> { username?: string; showPasswordRules?: boolean; showForgotPassword?: boolean; - inputId?: string; } const PasswordInputWithoutI18n = forwardRef( From da8d76fe0d65b0741a68174eefb5e5393b0417b2 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Mon, 26 Feb 2024 16:59:02 -0500 Subject: [PATCH 23/40] prevent extra i18n prop getting passed to input --- client/src/components/EmailInput.tsx | 5 +++-- client/src/components/PasswordInput.tsx | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/src/components/EmailInput.tsx b/client/src/components/EmailInput.tsx index c1f02cbab..a02cbcc41 100644 --- a/client/src/components/EmailInput.tsx +++ b/client/src/components/EmailInput.tsx @@ -15,10 +15,11 @@ interface EmailInputProps extends React.ComponentPropsWithoutRef<"input"> { setError: React.Dispatch>; showError: boolean; onChange: (e: ChangeEvent) => void; + i18nHash?: string; } const EmailInputWithoutI18n = forwardRef( - ({ i18n, email, error, setError, showError, onChange, ...props }, ref) => { + ({ i18n, i18nHash, email, error, setError, showError, onChange, ...props }, ref) => { const isBadEmailFormat = (value: string) => { /* valid email regex rules alpha numeric characters are ok, upper/lower case agnostic @@ -68,6 +69,6 @@ const EmailInputWithoutI18n = forwardRef( } ); -const EmailInput = withI18n()(EmailInputWithoutI18n); +const EmailInput = withI18n({ withHash: false })(EmailInputWithoutI18n); export default EmailInput; diff --git a/client/src/components/PasswordInput.tsx b/client/src/components/PasswordInput.tsx index 310e6d8c8..7c9fa0264 100644 --- a/client/src/components/PasswordInput.tsx +++ b/client/src/components/PasswordInput.tsx @@ -42,12 +42,14 @@ interface PasswordInputProps extends React.ComponentPropsWithoutRef<"input"> { username?: string; showPasswordRules?: boolean; showForgotPassword?: boolean; + i18nHash?: string; } const PasswordInputWithoutI18n = forwardRef( ( { i18n, + i18nHash, labelText, username, password, From 85e3760396ef88b2150a0515847c416017374d03 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Mon, 26 Feb 2024 17:17:26 -0500 Subject: [PATCH 24/40] usertype input remove preventDefault bug radio not updating --- client/src/components/UserTypeInput.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/components/UserTypeInput.tsx b/client/src/components/UserTypeInput.tsx index 8bef77fd4..935f9e180 100644 --- a/client/src/components/UserTypeInput.tsx +++ b/client/src/components/UserTypeInput.tsx @@ -22,14 +22,13 @@ const UserTypeInputWithoutI18n = (props: UserTypeInputProps) => { const [activeRadio, setActiveRadio] = useState(""); const handleRadioChange = (e: ChangeEvent) => { - e.preventDefault(); const value = e.target.value; setActiveRadio(value); if (value === USER_TYPES.other) { setUserType(""); setError(true); } else { - onChange(e); + setUserType(value); setError(false); } }; From 3e5a4bb0c6603d50ee49848762b88790ece725d8 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Mon, 26 Feb 2024 17:17:54 -0500 Subject: [PATCH 25/40] add "didn't get link" line to on page verify reminder --- client/src/components/EmailAlertSignup.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/components/EmailAlertSignup.tsx b/client/src/components/EmailAlertSignup.tsx index 5d1325559..3a4cfc567 100644 --- a/client/src/components/EmailAlertSignup.tsx +++ b/client/src/components/EmailAlertSignup.tsx @@ -1,7 +1,6 @@ import React, { Fragment, useContext, useState } from "react"; import { withI18n, withI18nProps, I18n } from "@lingui/react"; -import { t } from "@lingui/macro"; import { Trans } from "@lingui/macro"; import Login from "./Login"; import { UserContext } from "./UserContext"; @@ -46,9 +45,10 @@ const BuildingSubscribeWithoutI18n = (props: BuildingSubscribeProps) => { Verify your email to start receiving updates. -
- {i18n._(t`Click the link we sent to ${email}. It may take a few minutes to arrive.`)} -
+ + Click the link we sent to {email}. It may take a few minutes to arrive. + + Didn’t get the link?
); }; From baa71c50b3c3dbb11badc05bf038612be50be3d0 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Wed, 28 Feb 2024 11:37:38 -0500 Subject: [PATCH 27/40] fix typo in usertype input error --- client/src/components/Login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index b1078dbea..c696dc2f2 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -431,7 +431,7 @@ const LoginWithoutI18n = (props: LoginProps) => { setUserType={setUserType} error={userTypeError} showError={userShowUserTypeError} - setError={setShowUserTypeError} + setError={setUserTypeError} onChange={onChangeUserType} /> )} From 200b1de08fa2e3fdf2ddc29e2a9b3907286aaeb6 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Wed, 28 Feb 2024 22:59:28 -0500 Subject: [PATCH 28/40] fix typo in email validation regex, add label as optional prop --- client/src/components/EmailInput.tsx | 13 ++++++++----- client/src/components/Login.tsx | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/client/src/components/EmailInput.tsx b/client/src/components/EmailInput.tsx index a02cbcc41..20eb2b2b6 100644 --- a/client/src/components/EmailInput.tsx +++ b/client/src/components/EmailInput.tsx @@ -16,17 +16,18 @@ interface EmailInputProps extends React.ComponentPropsWithoutRef<"input"> { showError: boolean; onChange: (e: ChangeEvent) => void; i18nHash?: string; + labelText?: string; } const EmailInputWithoutI18n = forwardRef( - ({ i18n, i18nHash, email, error, setError, showError, onChange, ...props }, ref) => { + ({ i18n, i18nHash, email, error, setError, showError, onChange, labelText, ...props }, ref) => { const isBadEmailFormat = (value: string) => { /* valid email regex rules alpha numeric characters are ok, upper/lower case agnostic username: leading \_ ok, chars \_\.\-\+ ok in all other positions domain name: chars \.\- ok as long as not leading. must end in a \. and at least two alphabet chars */ const pattern = - "^([a-zA-Z0-9_]+[a-zA-Z0-9+_.-]+@[a-zA-Z0-9]+[a-zA-Z0-9.-]+[a-zA-Z0-9]+.[a-zA-Z]{2,})$"; + "^([a-zA-Z0-9_]+[a-zA-Z0-9+_.-]+@[a-zA-Z0-9]+[a-zA-Z0-9.-]+[a-zA-Z0-9]+\\.[a-zA-Z]{2,})$"; // HTML input element has loose email validation requirements, so we check the input against a custom regex const passStrictRegex = value.match(pattern); @@ -43,9 +44,11 @@ const EmailInputWithoutI18n = forwardRef( return (
-
- -
+ {!!labelText && ( +
+ +
+ )} {showError && error && (
diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index c696dc2f2..862418814 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -409,6 +409,7 @@ const LoginWithoutI18n = (props: LoginProps) => { setError={setEmailError} showError={showEmailError} autoFocus={showRegisterModal && !email} + labelText={i18n._(t`Email address`)} /> )} {(isLoginStep || isRegisterAccountStep) && ( From cc51ec5b5e1ca5b9420a59d59ee975ed28bfa00d Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Wed, 28 Feb 2024 23:00:01 -0500 Subject: [PATCH 29/40] show password rules when showError (even if empty) --- client/src/components/PasswordInput.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/client/src/components/PasswordInput.tsx b/client/src/components/PasswordInput.tsx index 7c9fa0264..370da0a65 100644 --- a/client/src/components/PasswordInput.tsx +++ b/client/src/components/PasswordInput.tsx @@ -82,18 +82,15 @@ const PasswordInputWithoutI18n = forwardRef - Forgot your password? + {i18n._(t`Forgot your password?`)} )}
{showPasswordRules && (
{passwordRules.map((rule, i) => { - const ruleClass = !!password - ? password.match(rule.regex) - ? "valid" - : "invalid" - : ""; + const ruleClass = + !!password || showError ? (password.match(rule.regex) ? "valid" : "invalid") : ""; return ( {rule.label} From d0a60ffa9ce32e35479fce891af8d27f6e521c3e Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Wed, 28 Feb 2024 23:00:23 -0500 Subject: [PATCH 30/40] add error handling of inputs before updating credentials --- client/src/components/UserSettingField.tsx | 129 ++++++++++++++++++--- client/src/styles/AccountSettingsPage.scss | 4 +- 2 files changed, 113 insertions(+), 20 deletions(-) diff --git a/client/src/components/UserSettingField.tsx b/client/src/components/UserSettingField.tsx index bd29cd0a2..ca8c10a69 100644 --- a/client/src/components/UserSettingField.tsx +++ b/client/src/components/UserSettingField.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { withI18n, withI18nProps } from "@lingui/react"; import { t, Trans } from "@lingui/macro"; @@ -6,6 +6,11 @@ import { t, Trans } from "@lingui/macro"; import "styles/EmailAlertSignup.css"; import PasswordInput from "./PasswordInput"; import { useInput } from "util/helpers"; +import { UserContext } from "./UserContext"; +import { JustfixUser } from "state-machine"; +import EmailInput from "./EmailInput"; +import AuthClient from "./AuthClient"; +import { Alert } from "./Alert"; type PasswordSettingFieldProps = withI18nProps & { onSubmit: (currentPassword: string, newPassword: string) => void; @@ -13,11 +18,14 @@ type PasswordSettingFieldProps = withI18nProps & { const PasswordSettingFieldWithoutI18n = (props: PasswordSettingFieldProps) => { const { i18n, onSubmit } = props; + const userContext = useContext(UserContext); + const { email } = userContext.user as JustfixUser; const { value: currentPassword, error: currentPasswordError, showError: showCurrentPasswordError, setError: setCurrentPasswordError, + setShowError: setShowCurrentPasswordError, onChange: onChangeCurrentPassword, } = useInput(""); const { @@ -25,16 +33,52 @@ const PasswordSettingFieldWithoutI18n = (props: PasswordSettingFieldProps) => { error: newPasswordError, showError: showNewPasswordError, setError: setNewPasswordError, + setShowError: setShowNewPasswordError, onChange: onChangeNewPassword, } = useInput(""); - // const [showCurrentPassword, setShowCurrentPassword] = useState(false); + const [invalidAuthError, setInvalidAuthError] = useState(false); + + const handleSubmit = async () => { + setInvalidAuthError(false); + setShowCurrentPasswordError(false); + setShowNewPasswordError(false); + + if (!currentPassword) { + setCurrentPasswordError(true); + setShowCurrentPasswordError(true); + throw new Error("Current password missing"); + } + + if (!newPassword || newPasswordError) { + setNewPasswordError(true); + setShowNewPasswordError(true); + throw new Error("New password format error"); + } + + // context doesn't update immediately so need to reurn user to check verified status + const resp = await userContext.login(email, currentPassword); + + if (!!resp?.error) { + setInvalidAuthError(true); + throw new Error("Incorrect current password"); + } - const handleSubmit = () => { onSubmit(currentPassword, newPassword); }; return ( + {invalidAuthError && ( + + {i18n._(t`The old password you entered is incorrect`)} + + )} { const { i18n, currentValue, onSubmit } = props; - const [value, setValue] = useState(currentValue); - - const handleValueChange = (e: React.ChangeEvent) => { - setValue(e.target.value); - }; - - const handleSubmit = () => { - onSubmit(value); + const userContext = useContext(UserContext); + const { email: oldEmail } = userContext.user as JustfixUser; + const [existingUserError, setExistingUserError] = useState(false); + const { + value: email, + error: emailError, + showError: showEmailError, + setError: setEmailError, + setShowError: setShowEmailError, + onChange: onChangeEmail, + } = useInput(oldEmail); + + const handleSubmit = async () => { + setExistingUserError(false); + setShowEmailError(false); + + if (email === oldEmail) { + return; + } + + if (!email || emailError) { + setEmailError(true); + setShowEmailError(true); + throw new Error("Email format error"); + } + + if (!!email && !emailError) { + const existingUser = await AuthClient.isEmailAlreadyUsed(email); + if (existingUser) { + setExistingUserError(true); + throw new Error("Existing user error"); + } + } + + onSubmit(email); }; return ( @@ -84,13 +155,26 @@ const EmailSettingFieldWithoutI18n = (props: EmailSettingFieldProps) => { onSubmit={handleSubmit} > Email address + {existingUserError && ( + + {i18n._(t`That email is already used.`)} + + )} This is used for logging in and for receiving weekly data updates. - ); @@ -101,16 +185,25 @@ export const EmailSettingField = withI18n()(EmailSettingFieldWithoutI18n); type UserSettingFieldProps = withI18nProps & { title: string; preview: string; - onSubmit: () => void; + onSubmit: () => Promise; children: React.ReactNode; }; const UserSettingFieldWithoutI18n = (props: UserSettingFieldProps) => { const { title, preview, onSubmit, children } = props; const [editing, setEditing] = useState(false); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit().then( + (response) => setEditing(false), + (error) => {} + ); + }; + return (
-
+ {editing ? ( <> {children} diff --git a/client/src/styles/AccountSettingsPage.scss b/client/src/styles/AccountSettingsPage.scss index 0d5950df4..7da86a7d3 100644 --- a/client/src/styles/AccountSettingsPage.scss +++ b/client/src/styles/AccountSettingsPage.scss @@ -45,13 +45,14 @@ margin: 3.2rem 1.6rem; gap: 2.4rem; + .email-input-field, .password-input-field { display: flex; flex-direction: column; } p { - margin: -1rem 0 0.6rem 0; + margin: 0; } } @@ -84,7 +85,6 @@ font-size: 1.3rem !important; text-align: left; - margin-bottom: 1.6rem; } .password-input-rules { From 46fc355b6d616dbbb15fd51d549fb792d37fd24e Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Thu, 29 Feb 2024 13:00:04 -0500 Subject: [PATCH 31/40] add missing pw field label --- client/src/components/UserSettingField.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/UserSettingField.tsx b/client/src/components/UserSettingField.tsx index ca8c10a69..3cd5b1a7a 100644 --- a/client/src/components/UserSettingField.tsx +++ b/client/src/components/UserSettingField.tsx @@ -68,6 +68,7 @@ const PasswordSettingFieldWithoutI18n = (props: PasswordSettingFieldProps) => { return ( + Password {invalidAuthError && ( Date: Thu, 29 Feb 2024 18:53:38 -0500 Subject: [PATCH 32/40] change email pattern to regex instead of string --- client/src/components/EmailInput.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/components/EmailInput.tsx b/client/src/components/EmailInput.tsx index 20eb2b2b6..2bffec466 100644 --- a/client/src/components/EmailInput.tsx +++ b/client/src/components/EmailInput.tsx @@ -26,8 +26,7 @@ const EmailInputWithoutI18n = forwardRef( alpha numeric characters are ok, upper/lower case agnostic username: leading \_ ok, chars \_\.\-\+ ok in all other positions domain name: chars \.\- ok as long as not leading. must end in a \. and at least two alphabet chars */ - const pattern = - "^([a-zA-Z0-9_]+[a-zA-Z0-9+_.-]+@[a-zA-Z0-9]+[a-zA-Z0-9.-]+[a-zA-Z0-9]+\\.[a-zA-Z]{2,})$"; + const pattern = /^([a-zA-Z0-9_]+[a-zA-Z0-9+_.-]+@[a-zA-Z0-9]+[a-zA-Z0-9.-]+[a-zA-Z0-9]+\.[a-zA-Z]{2,})$/; // HTML input element has loose email validation requirements, so we check the input against a custom regex const passStrictRegex = value.match(pattern); From d7e1d3ac9ba50a16ef74552ec2837f28fe5b4b19 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Thu, 29 Feb 2024 18:54:05 -0500 Subject: [PATCH 33/40] add password error label, fix label styles on account page --- client/src/components/PasswordInput.tsx | 10 +++++++++- client/src/components/UserSettingField.tsx | 12 +++++++++--- client/src/styles/AccountSettingsPage.scss | 2 +- client/src/styles/Login.scss | 4 ---- client/src/styles/Password.scss | 17 +++++++++++++++++ 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/client/src/components/PasswordInput.tsx b/client/src/components/PasswordInput.tsx index 370da0a65..2e2660cff 100644 --- a/client/src/components/PasswordInput.tsx +++ b/client/src/components/PasswordInput.tsx @@ -9,7 +9,7 @@ import { LocaleLink } from "i18n"; import { createWhoOwnsWhatRoutePaths } from "routes"; import { I18n } from "@lingui/core"; import { t } from "@lingui/macro"; -import { HideIcon, ShowIcon } from "./Icons"; +import { AlertIcon, HideIcon, ShowIcon } from "./Icons"; import classNames from "classnames"; type PasswordRule = { @@ -99,6 +99,14 @@ const PasswordInputWithoutI18n = forwardRef )} + {!showPasswordRules && showError && error && ( +
+ + + {i18n._(t`Please enter password.`)} + +
+ )}
{ return ( - Password + + Password + {invalidAuthError && ( { preview={currentValue} onSubmit={handleSubmit} > - Email address + + Email address + {existingUserError && ( { ) : ( <> - {title} + + {title} +
{preview}
diff --git a/client/src/styles/AccountSettingsPage.scss b/client/src/styles/AccountSettingsPage.scss index 7ad99a686..018d75e54 100644 --- a/client/src/styles/AccountSettingsPage.scss +++ b/client/src/styles/AccountSettingsPage.scss @@ -57,7 +57,7 @@ // vars marked !important bc typography mixin is not available for inconsolata font-weight: 600 !important; font-family: $wow-font !important; - font-size: 1.3rem !important; + font-size: 1.4rem !important; text-transform: none !important; text-align: left; margin-bottom: 0.8rem; @@ -88,6 +88,7 @@ .user-setting-actions { display: flex; justify-content: left; + align-items: center; margin-top: 1.6rem; @include for-phone-only() { @@ -95,7 +96,14 @@ } input[type="submit"] { - margin: 0 1.6rem 0 0 !important; + margin: 0 4.8rem 0 0 !important; + } + + .button.is-text { + @include body-standard; + font-size: 1.8rem; + font-weight: 500; + text-decoration: none; } } } From 31572bff21d327e4b89f8fac2bdce4e4b57fe779 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Tue, 5 Mar 2024 11:59:04 -0500 Subject: [PATCH 37/40] handle un-verifying on email change & display callout --- client/src/components/UserContext.tsx | 2 +- client/src/components/UserSettingField.tsx | 20 ++++++++++++++++++-- client/src/styles/AccountSettingsPage.scss | 10 ++++++++++ client/src/styles/_callout.scss | 8 ++++++++ jfauthprovider/views.py | 5 ++++- 5 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 client/src/styles/_callout.scss diff --git a/client/src/components/UserContext.tsx b/client/src/components/UserContext.tsx index 28cac190b..7b270a7ce 100644 --- a/client/src/components/UserContext.tsx +++ b/client/src/components/UserContext.tsx @@ -178,7 +178,7 @@ export const UserContextProvider = ({ children }: { children: React.ReactNode }) if (user) { const asyncUpdateEmail = async () => { const response = await AuthClient.updateEmail(email); - setUser({ ...user, email: response.email }); + setUser({ ...user, email: response.email, verified: false }); }; asyncUpdateEmail(); } diff --git a/client/src/components/UserSettingField.tsx b/client/src/components/UserSettingField.tsx index b9935e11b..f73b009df 100644 --- a/client/src/components/UserSettingField.tsx +++ b/client/src/components/UserSettingField.tsx @@ -117,7 +117,7 @@ type EmailSettingFieldProps = withI18nProps & { const EmailSettingFieldWithoutI18n = (props: EmailSettingFieldProps) => { const { i18n, currentValue, onSubmit } = props; const userContext = useContext(UserContext); - const { email: oldEmail } = userContext.user as JustfixUser; + const { email: oldEmail, verified } = userContext.user as JustfixUser; const [existingUserError, setExistingUserError] = useState(false); const { value: email, @@ -153,11 +153,25 @@ const EmailSettingFieldWithoutI18n = (props: EmailSettingFieldProps) => { onSubmit(email); }; + const verifyCallout = !verified ? ( +
+ + Email address not verified. Click the link we sent to {email} start receiving emails. + +
+
+ +
+ ) : undefined; + return ( {existingUserError && ( Promise; children: React.ReactNode; + verifyCallout?: React.ReactNode; }; const UserSettingFieldWithoutI18n = (props: UserSettingFieldProps) => { - const { title, preview, onSubmit, children } = props; + const { title, preview, onSubmit, children, verifyCallout } = props; const [editing, setEditing] = useState(false); const handleSubmit = (e: React.FormEvent) => { @@ -233,6 +248,7 @@ const UserSettingFieldWithoutI18n = (props: UserSettingFieldProps) => { Edit
+ {!!verifyCallout && verifyCallout} )} diff --git a/client/src/styles/AccountSettingsPage.scss b/client/src/styles/AccountSettingsPage.scss index 018d75e54..bfcc239ce 100644 --- a/client/src/styles/AccountSettingsPage.scss +++ b/client/src/styles/AccountSettingsPage.scss @@ -1,5 +1,6 @@ @import "_vars.scss"; @import "_typography.scss"; +@import "_callout.scss"; .AccountSettingsPage { h4 { @@ -75,6 +76,15 @@ margin-top: 2rem; } } + + .jf-callout { + margin-top: 1.6rem; + display: flex; + flex-direction: column; + .button.is-text { + width: fit-content; + } + } } form > div { diff --git a/client/src/styles/_callout.scss b/client/src/styles/_callout.scss new file mode 100644 index 000000000..ec89c0317 --- /dev/null +++ b/client/src/styles/_callout.scss @@ -0,0 +1,8 @@ +@import "_vars.scss"; +@import "_typography.scss"; + +.jf-callout { + @include desktop-text-small; + background-color: $justfix-white-200; + padding: 1.2rem; +} diff --git a/jfauthprovider/views.py b/jfauthprovider/views.py index 975764821..25972715e 100644 --- a/jfauthprovider/views.py +++ b/jfauthprovider/views.py @@ -60,7 +60,10 @@ def logout(request): @api def update(request): try: - post_data = {"new_email": request.POST.get("new_email")} + post_data = { + "new_email": request.POST.get("new_email"), + "origin": request.headers["Origin"], + } return authenticated_request( "user/", From 7bcd0e5b685356bc468fc04c43b59e719b0216b0 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Wed, 6 Mar 2024 11:47:29 -0500 Subject: [PATCH 38/40] adjust for 16px base font --- client/src/styles/AccountSettingsPage.scss | 22 +++++++++++----------- client/src/styles/Password.scss | 15 +++++++-------- client/src/styles/_callout.scss | 2 +- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/client/src/styles/AccountSettingsPage.scss b/client/src/styles/AccountSettingsPage.scss index 9a9f55f0b..102005c58 100644 --- a/client/src/styles/AccountSettingsPage.scss +++ b/client/src/styles/AccountSettingsPage.scss @@ -43,12 +43,12 @@ form { display: flex; flex-direction: column; - margin: 2.6rem 1.6rem; + margin: 1.625rem 1rem; .page-level-alert { - margin-bottom: 2.6rem; + margin-bottom: 1.625rem; svg { - margin-right: 0.8rem; + margin-right: 0.5rem; vertical-align: bottom; } } @@ -58,14 +58,14 @@ // vars marked !important bc typography mixin is not available for inconsolata font-weight: 600 !important; font-family: $wow-font !important; - font-size: 1.4rem !important; + font-size: 0.875rem !important; text-transform: none !important; text-align: left; - margin-bottom: 0.8rem; + margin-bottom: 0.5rem; } p { - margin-bottom: 0.6rem; + margin-bottom: 0.375rem; } .email-input-field, @@ -73,12 +73,12 @@ display: flex; flex-direction: column; &:has(#new-password-input) { - margin-top: 2rem; + margin-top: 1.25rem; } } .jf-callout { - margin-top: 1.6rem; + margin-top: 1rem; display: flex; flex-direction: column; .button.is-text { @@ -99,19 +99,19 @@ display: flex; justify-content: left; align-items: center; - margin-top: 1.6rem; + margin-top: 1rem; @include for-phone-only() { justify-content: center; } input[type="submit"] { - margin: 0 4.8rem 0 0 !important; + margin: 0 3rem 0 0 !important; } .button.is-text { @include body-standard; - font-size: 1.8rem; + font-size: 1.125rem; font-weight: 500; text-decoration: none; } diff --git a/client/src/styles/Password.scss b/client/src/styles/Password.scss index c1b9287c3..fb806cf86 100644 --- a/client/src/styles/Password.scss +++ b/client/src/styles/Password.scss @@ -60,7 +60,7 @@ .password-input-rules { display: flex; flex-direction: column; - margin-bottom: 0.6rem; + margin-bottom: 0.375rem; } .password-input-rule { @@ -68,7 +68,6 @@ font-family: $wow-font !important; font-size: 0.8125rem !important; - // font-weight: 500 !important; line-height: 120%; text-transform: none !important; color: $justfix-black; @@ -83,9 +82,9 @@ } svg { - width: 1.4rem; - height: 1.4rem; - margin-right: 0.5rem; + width: 0.875rem; + height: 0.875rem; + margin-right: 0.3125rem; vertical-align: text-bottom; } } @@ -97,12 +96,12 @@ span { display: flex; align-items: center; - font-size: 1.3rem; + font-size: 0.8125rem; color: #ba4300; - margin-bottom: 0.5rem; + margin-bottom: 0.3125rem; svg { - margin-right: 0.75rem; + margin-right: 0.46875rem; } } } diff --git a/client/src/styles/_callout.scss b/client/src/styles/_callout.scss index ec89c0317..6cb75ce10 100644 --- a/client/src/styles/_callout.scss +++ b/client/src/styles/_callout.scss @@ -4,5 +4,5 @@ .jf-callout { @include desktop-text-small; background-color: $justfix-white-200; - padding: 1.2rem; + padding: 0.75rem; } From 0ef1dd8a9d828133f84782337a5a0e202d8e3811 Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Wed, 6 Mar 2024 12:03:46 -0500 Subject: [PATCH 39/40] fix some font sizes --- client/src/components/UserSettingField.tsx | 6 +++++- client/src/styles/AccountSettingsPage.scss | 4 ++++ client/src/styles/_typography.scss | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/client/src/components/UserSettingField.tsx b/client/src/components/UserSettingField.tsx index f73b009df..8b1330d62 100644 --- a/client/src/components/UserSettingField.tsx +++ b/client/src/components/UserSettingField.tsx @@ -244,7 +244,11 @@ const UserSettingFieldWithoutI18n = (props: UserSettingFieldProps) => {
{preview} -
diff --git a/client/src/styles/AccountSettingsPage.scss b/client/src/styles/AccountSettingsPage.scss index 102005c58..f3c54ae88 100644 --- a/client/src/styles/AccountSettingsPage.scss +++ b/client/src/styles/AccountSettingsPage.scss @@ -93,6 +93,10 @@ span { flex: 1; } + span, + .edit-button { + font-size: 1rem; + } } .user-setting-actions { diff --git a/client/src/styles/_typography.scss b/client/src/styles/_typography.scss index 340208268..9103aa09a 100644 --- a/client/src/styles/_typography.scss +++ b/client/src/styles/_typography.scss @@ -49,7 +49,7 @@ $eyebrow-font: "Suisse Int'l Mono", "Courier New", Courier, monospace; @mixin desktop-text-small { @include body-standard(); - font-size: 0.55rem; + font-size: 0.875rem; } @mixin desktop-text-small-bold { From b4a28623cd4b88a0f9d10df4777eefcde7755e8a Mon Sep 17 00:00:00 2001 From: Maxwell Austensen Date: Wed, 6 Mar 2024 14:39:07 -0500 Subject: [PATCH 40/40] simplify PW rules/errors, cut duplicate cors setting --- client/src/components/PasswordInput.tsx | 20 +++++++++++--------- project/settings.py | 4 ---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/client/src/components/PasswordInput.tsx b/client/src/components/PasswordInput.tsx index 8d14e80a1..33a85f68c 100644 --- a/client/src/components/PasswordInput.tsx +++ b/client/src/components/PasswordInput.tsx @@ -86,7 +86,7 @@ const PasswordInputWithoutI18n = forwardRef )}
- {showPasswordRules && ( + {showPasswordRules ? (
{passwordRules.map((rule, i) => { let ruleClass = ""; @@ -108,14 +108,16 @@ const PasswordInputWithoutI18n = forwardRef - )} - {!showPasswordRules && showError && error && ( -
- - - {i18n._(t`Please enter password.`)} - -
+ ) : ( + showError && + error && ( +
+ + + {i18n._(t`Please enter password.`)} + +
+ ) )}
str: r"https://deploy-preview-(?:\d{1,4})--wow-django-dev\.netlify\.app", ] -CORS_ALLOWED_ORIGIN_REGEXES = [ - r"https://\w+\.deploy-preview-(?:\d{1,4})--wow-django-dev\.netlify\.app" -] - # This is based off the default Django logging configuration: # https://github.com/django/django/blob/master/django/utils/log.py LOGGING = {