Skip to content

Commit

Permalink
Multi-step log in/sign up modal, with user type (#849)
Browse files Browse the repository at this point in the history
- adds ability for account sign up to happen all within a modal from the building page
- adds a required step for account sign up, asking user type (multiple-choice or write in)
- adds verify email/ re-send email step to the end of log in/sign up flow
- refactors email input into separate component (like PasswordInput
  • Loading branch information
austensen authored Mar 6, 2024
1 parent c5fba60 commit 6a0a1a8
Show file tree
Hide file tree
Showing 24 changed files with 1,083 additions and 471 deletions.
8 changes: 6 additions & 2 deletions client/src/components/AuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,12 @@ const clearUser = () => (_user = undefined);
* Authenticates a user with the given email and password.
* Creates an account for this user if one does not already exist.
*/
const register = async (username: string, password: string) => {
const json = await postAuthRequest(`${BASE_URL}auth/register`, { username, password });
const register = async (username: string, password: string, userType: string) => {
const json = await postAuthRequest(`${BASE_URL}auth/register`, {
username,
password,
user_type: userType,
});
fetchUser();
return json;
};
Expand Down
93 changes: 28 additions & 65 deletions client/src/components/EmailAlertSignup.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,7 +10,8 @@ import { LocaleLink as Link } from "../i18n";
import "styles/EmailAlertSignup.css";
import { JustfixUser } from "state-machine";
import AuthClient from "./AuthClient";
import { AlertIconOutline, SubscribedIcon } from "./Icons";
import { SubscribedIcon } from "./Icons";
import { Alert } from "./Alert";
import Modal from "./Modal";

const SUBSCRIPTION_LIMIT = 15;
Expand Down Expand Up @@ -49,29 +49,27 @@ const BuildingSubscribeWithoutI18n = (props: BuildingSubscribeProps) => {
const showEmailVerification = (i18n: any) => {
return (
<div className="building-subscribe-status">
<div>
<div className="status-title">
<AlertIconOutline />
<Trans>Email verification required</Trans>
</div>
<div className="status-description">
{i18n._(t`Click the link we sent to ${email}. It may take a few minutes to arrive.`)}
</div>
<button
className="button is-secondary is-full-width"
onClick={() => AuthClient.resendVerifyEmail()}
>
<Trans>Resend email</Trans>
</button>
</div>
<Alert type="info">
<Trans>Verify your email to start receiving updates.</Trans>
</Alert>
<Trans render="div" className="status-description">
Click the link we sent to {email}. It may take a few minutes to arrive.
</Trans>
<Trans render="div">Didn’t get the link?</Trans>
<button
className="button is-secondary is-full-width"
onClick={() => AuthClient.resendVerifyEmail()}
>
<Trans>Resend email</Trans>
</button>
</div>
);
};
return (
<I18n>
{({ i18n }) => (
<>
<div className="table-content building-subscribe">
<div className="building-subscribe">
{!(subscriptions && !!subscriptions?.find((s) => s.bbl === bbl)) ? (
<button
className="button is-primary"
Expand All @@ -89,7 +87,6 @@ const BuildingSubscribeWithoutI18n = (props: BuildingSubscribeProps) => {
showEmailVerification(i18n)
)}
</div>

<Modal
key={1}
showModal={showSubscriptionLimitModal}
Expand Down Expand Up @@ -125,18 +122,15 @@ 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 (
<>
<div className="EmailAlertSignup card-body-table">
<div className="table-row">
<I18n>
{({ i18n }) => (
<div
title={i18n._(t`Get data updates for this building`)}
className="table-small-font"
>
<div className="table-small-font">
<label className="data-updates-label-container">
<span className="pill-new">
<Trans>NEW</Trans>
Expand All @@ -148,50 +142,19 @@ const EmailAlertSignupWithoutI18n = (props: EmailAlertProps) => {
)}
</label>
<div className="table-content">
{!user ? (
<Fragment>
<div className="email-description">
<Trans>
Each weekly email includes HPD Complaints, HPD Violations, and Eviction
Filings.
</Trans>
</div>
<Login
onBuildingPage={true}
onSuccess={(user: JustfixUser) => {
userContext.subscribe(bbl, housenumber, streetname, zip, boro, user);
!user.verified && setShowVerifyModal(true);
}}
/>
</Fragment>
) : (
{user && !loginRegisterInProgress ? (
<BuildingSubscribe {...props} />
) : (
<Login
registerInModal
onBuildingPage
setLoginRegisterInProgress={setLoginRegisterInProgress}
onSuccess={(user: JustfixUser) => {
userContext.subscribe(bbl, housenumber, streetname, zip, boro, user);
}}
/>
)}
</div>
<Modal
key={1}
showModal={showVerifyModal}
width={40}
onClose={() => setShowVerifyModal(false)}
>
<Trans render="h4">Verify your email to start receiving updates</Trans>
{i18n._(
t`Click the link we sent to ${user?.email}. It may take a few minutes to arrive.`
)}
<br />
<br />
<Trans>
Once your email has been verified, you’ll be signed up for Data Updates.
</Trans>
<br />
<br />
<button
className="button is-secondary is-full-width"
onClick={() => AuthClient.resendVerifyEmail()}
>
<Trans>Resend email</Trans>
</button>
</Modal>
</div>
)}
</I18n>
Expand Down
74 changes: 74 additions & 0 deletions client/src/components/EmailInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ChangeEvent, forwardRef } from "react";
import { 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";
import classNames from "classnames";

interface EmailInputProps extends React.ComponentPropsWithoutRef<"input"> {
i18n: I18n;
email: string;
error: boolean;
setError: React.Dispatch<React.SetStateAction<boolean>>;
showError: boolean;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
i18nHash?: string;
}

const EmailInputWithoutI18n = forwardRef<HTMLInputElement, EmailInputProps>(
({ 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
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,})$";

// HTML input element has loose email validation requirements, so we check the input against a custom regex
const passStrictRegex = value.match(pattern);
const passAutoValidation = document.querySelectorAll("input:invalid").length === 0;

return !passAutoValidation || !passStrictRegex;
};

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange(e);
const emailIsInvalid = isBadEmailFormat(e.target.value);
setError(emailIsInvalid);
};

return (
<div className="email-input-field">
<div className="email-input-label">
<label htmlFor="email-input">{i18n._(t`Email address`)}</label>
</div>
{showError && error && (
<div className="email-input-errors">
<span id="input-field-error">
<AlertIcon />
{i18n._(t`Please enter a valid email address.`)}
</span>
</div>
)}
<div className="email-input">
<input
type="email"
id="email-input"
className={classNames("input", { invalid: showError && error })}
onChange={handleChange}
value={email}
{...props}
/>
</div>
</div>
);
}
);

const EmailInput = withI18n({ withHash: false })(EmailInputWithoutI18n);

export default EmailInput;
8 changes: 4 additions & 4 deletions client/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ export const CheckIcon = (props: SVGProps<SVGSVGElement>) => (

export const SubscribedIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width="36"
width="31"
height="30"
viewBox="0 0 36 30"
viewBox="0 0 31 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect x="0.5" width="34.6667" height="30" rx="15" fill="#242323" />
<path d="M10.5 15.3333L15.1667 20L25.1667 10" stroke="#F2F2F2" stroke-width="2" />
<rect x="0.5" width="30" height="30" rx="15" fill="#242323" />
<path d="M8 15.3333L12.6667 20L22.6667 10" stroke="#F2F2F2" strokeWidth="2" />
</svg>
);

Expand Down
Loading

0 comments on commit 6a0a1a8

Please sign in to comment.