Skip to content

Commit

Permalink
Add validation/error handling to account settings update credentials (#…
Browse files Browse the repository at this point in the history
…854)

This PR adds input validation and error handling to the forms for updating user email and password on the account settings page to ensure the new email is valid and not already in use, and that the new password satisfies rules. It swaps in the new <EmailInput>, and adapts the existing validation/error handling logic from ` to show input- and page-level alerts. It prevents the default form reload and controls the toggling of edit mode after successful submission. Also adds an option to show an input-level error for password when rules aren't shown. Also fixes some spacing/styles that might have gotten messed up with the modal login changes. Also adds an input-level error state for password when the rule validation messages aren't shown (eg. for empty input when logging in).
There is now a "callout" box under email when not editing if you haven't verified your email. And when you successfully change your email we mark it un-verified and send a verify email on the server side (JustFixNYC/auth-provider#17) and then the callout appears.
  • Loading branch information
austensen authored Mar 7, 2024
1 parent 6a0a1a8 commit 8182046
Show file tree
Hide file tree
Showing 13 changed files with 287 additions and 95 deletions.
14 changes: 8 additions & 6 deletions client/src/components/EmailInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ interface EmailInputProps extends React.ComponentPropsWithoutRef<"input"> {
showError: boolean;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
i18nHash?: string;
labelText?: string;
}

const EmailInputWithoutI18n = forwardRef<HTMLInputElement, EmailInputProps>(
({ 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,})$";
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);
Expand All @@ -43,9 +43,11 @@ const EmailInputWithoutI18n = forwardRef<HTMLInputElement, EmailInputProps>(

return (
<div className="email-input-field">
<div className="email-input-label">
<label htmlFor="email-input">{i18n._(t`Email address`)}</label>
</div>
{!!labelText && (
<div className="email-input-label">
<label htmlFor="email-input">{labelText}</label>
</div>
)}
{showError && error && (
<div className="email-input-errors">
<span id="input-field-error">
Expand Down
25 changes: 19 additions & 6 deletions client/src/components/Icons.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import React, { SVGProps } from "react";

export const DotIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width="20"
height="21"
viewBox="0 0 20 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<circle cx="9.5" cy="10.1751" r="1.5" fill="currentcolor" />
</svg>
);

export const CloseIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
fill="none"
Expand Down Expand Up @@ -134,21 +147,21 @@ export const AlertIconOutline = (props: SVGProps<SVGSVGElement>) => (
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M10 18C5.58857 18 2 14.4113 2 10C2 5.58865 5.58865 2 10 2C14.4113 2 18 5.58865 18 10C18 14.3886 14.4113 18 10 18ZM10 3.34857C6.34278 3.34857 3.37143 6.32004 3.37143 9.97714C3.37143 13.6344 6.3429 16.6057 10 16.6057C13.6572 16.6057 16.6286 13.6342 16.6286 9.97714C16.6286 6.32004 13.6573 3.34857 10 3.34857Z"
fill="#242323"
fill="#FAF8F4"
/>
<path
d="M10.0002 11.1655C9.61166 11.1655 9.31445 10.8683 9.31445 10.4798V6.98263C9.31445 6.59408 9.61164 6.29688 10.0002 6.29688C10.3888 6.29688 10.686 6.59407 10.686 6.98263V10.4798C10.686 10.8685 10.3888 11.1655 10.0002 11.1655Z"
fill="#242323"
d="M9.99996 11.1657C9.61141 11.1657 9.31421 10.8685 9.31421 10.48V6.98281C9.31421 6.59426 9.6114 6.29706 9.99996 6.29706C10.3885 6.29706 10.6857 6.59425 10.6857 6.98281V10.48C10.6857 10.8687 10.3885 11.1657 9.99996 11.1657Z"
fill="#FAF8F4"
/>
<path
d="M10.0002 13.6799C9.81733 13.6799 9.63446 13.6114 9.52013 13.4743C9.38301 13.3371 9.31445 13.1771 9.31445 12.9942C9.31445 12.9484 9.31445 12.9028 9.33725 12.8571C9.33725 12.8113 9.36005 12.7657 9.38301 12.7199C9.40581 12.6742 9.42877 12.6514 9.45157 12.6056C9.47437 12.5599 9.49733 12.5371 9.54293 12.4913C9.79437 12.2399 10.2515 12.2399 10.5029 12.4913C10.5257 12.5141 10.5715 12.5599 10.5943 12.6056C10.6171 12.6514 10.64 12.6742 10.6628 12.7199C10.6856 12.7657 10.6856 12.8113 10.7086 12.8571C10.7086 12.9028 10.7314 12.9484 10.7314 12.9942C10.7314 13.1771 10.6628 13.3599 10.5257 13.4743C10.366 13.6114 10.1831 13.6799 10.0002 13.6799H10.0002Z"
fill="#242323"
d="M9.99997 13.68C9.81709 13.68 9.63422 13.6114 9.51989 13.4743C9.38277 13.3372 9.31421 13.1771 9.31421 12.9942C9.31421 12.9485 9.31421 12.9029 9.33701 12.8571C9.33701 12.8114 9.35981 12.7658 9.38277 12.72C9.40557 12.6743 9.42853 12.6514 9.45133 12.6057C9.47413 12.5599 9.49709 12.5371 9.54269 12.4914C9.79412 12.2399 10.2512 12.2399 10.5027 12.4914C10.5255 12.5142 10.5712 12.5599 10.594 12.6057C10.6168 12.6514 10.6398 12.6743 10.6626 12.72C10.6854 12.7658 10.6854 12.8114 10.7084 12.8571C10.7084 12.9029 10.7312 12.9485 10.7312 12.9942C10.7312 13.1771 10.6626 13.36 10.5255 13.4743C10.3657 13.6114 10.1829 13.68 9.99998 13.68H9.99997Z"
fill="#FAF8F4"
/>
</svg>
);
Expand Down
1 change: 1 addition & 0 deletions client/src/components/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ const LoginWithoutI18n = (props: LoginProps) => {
setError={setEmailError}
showError={showEmailError}
autoFocus={showRegisterModal && !email}
labelText={i18n._(t`Email address`)}
/>
)}
{(isLoginStep || isRegisterAccountStep) && (
Expand Down
33 changes: 25 additions & 8 deletions client/src/components/PasswordInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, CheckIcon, DotIcon, HideIcon, ShowIcon } from "./Icons";
import classNames from "classnames";

type PasswordRule = {
Expand Down Expand Up @@ -82,25 +82,42 @@ const PasswordInputWithoutI18n = forwardRef<HTMLInputElement, PasswordInputProps
<LocaleLink
to={`${account.forgotPassword}?email=${encodeURIComponent(username || "")}`}
>
Forgot your password?
{i18n._(t`Forgot your password?`)}
</LocaleLink>
)}
</div>
{showPasswordRules && (
{showPasswordRules ? (
<div className="password-input-rules">
{passwordRules.map((rule, i) => {
const ruleClass = !!password
? password.match(rule.regex)
? "valid"
: "invalid"
: "";
let ruleClass = "";
let RuleIcon = <DotIcon />;
if (!!password || showError) {
if (password.match(rule.regex)) {
ruleClass = "valid";
RuleIcon = <CheckIcon />;
} else {
ruleClass = "invalid";
RuleIcon = <AlertIcon />;
}
}
return (
<span className={`password-input-rule ${ruleClass}`} key={`rule-${i}`}>
{RuleIcon}
{rule.label}
</span>
);
})}
</div>
) : (
showError &&
error && (
<div className="password-input-errors">
<span id="input-field-error">
<AlertIcon />
{i18n._(t`Please enter password.`)}
</span>
</div>
)
)}
<div className="password-input">
<input
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/UserContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,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();
}
Expand Down
Loading

0 comments on commit 8182046

Please sign in to comment.