From 6517400ae2220e0f01c97a8e975bb833b6e78925 Mon Sep 17 00:00:00 2001 From: Kyle Shanks Date: Thu, 30 Nov 2023 14:02:18 -0500 Subject: [PATCH] [C-3414] Update password reset modals to use harmony components (#6817) --- .../change-password/ChangePassword.tsx | 3 +- .../PasswordResetModal.module.css | 11 - .../password-reset/PasswordResetModal.tsx | 53 +-- .../sign-on/EnterPassword.module.css | 137 -------- .../src/components/sign-on/EnterPassword.tsx | 322 ++++-------------- packages/web/src/pages/modals/Modals.tsx | 2 +- .../web/src/pages/sign-in-page/SignInPage.tsx | 11 +- .../desktop/ForgotPasswordHelper.module.css | 56 +-- .../desktop/ForgotPasswordHelper.tsx | 68 ++-- .../components/EnterPasswordSection.tsx | 23 ++ .../sign-up-page/pages/CreatePasswordPage.tsx | 14 +- 11 files changed, 170 insertions(+), 530 deletions(-) delete mode 100644 packages/web/src/components/sign-on/EnterPassword.module.css create mode 100644 packages/web/src/pages/sign-up-page/components/EnterPasswordSection.tsx diff --git a/packages/web/src/components/change-password/ChangePassword.tsx b/packages/web/src/components/change-password/ChangePassword.tsx index 36e06a620ea..a8ebea3996f 100644 --- a/packages/web/src/components/change-password/ChangePassword.tsx +++ b/packages/web/src/components/change-password/ChangePassword.tsx @@ -91,8 +91,7 @@ export const ChangePassword = ({ return ( } - isMobile={true} + continueIcon={IconLock} onSubmit={onNewPasswordSubmitted} /> ) diff --git a/packages/web/src/components/password-reset/PasswordResetModal.module.css b/packages/web/src/components/password-reset/PasswordResetModal.module.css index b3be4d4215b..e5c8d35413e 100644 --- a/packages/web/src/components/password-reset/PasswordResetModal.module.css +++ b/packages/web/src/components/password-reset/PasswordResetModal.module.css @@ -7,10 +7,6 @@ width: 100%; } -.content { - padding: 16px; -} - .modalTitle { color: var(--neutral-light-4); font-size: var(--font-l); @@ -18,10 +14,3 @@ text-transform: uppercase; letter-spacing: 0.5px; } - -.helpText { - font-size: var(--font-m); - line-height: 20px; - margin: 0 auto 20px; - text-align: center; -} diff --git a/packages/web/src/components/password-reset/PasswordResetModal.tsx b/packages/web/src/components/password-reset/PasswordResetModal.tsx index e2af279ee21..be495a4ed00 100644 --- a/packages/web/src/components/password-reset/PasswordResetModal.tsx +++ b/packages/web/src/components/password-reset/PasswordResetModal.tsx @@ -1,17 +1,16 @@ -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' import { Status, accountSelectors } from '@audius/common' +import { Flex, Text } from '@audius/harmony' import { Modal } from '@audius/stems' -import { connect } from 'react-redux' -import { Dispatch } from 'redux' +import { useDispatch, useSelector } from 'react-redux' import EnterPassword from 'components/sign-on/EnterPassword' -import { AppState } from 'store/types' -import { isMobile } from 'utils/clientUtil' import styles from './PasswordResetModal.module.css' import { changePassword } from './store/actions' import { getStatus } from './store/selectors' + const { getNeedsAccountRecovery } = accountSelectors const RESET_REQUIRED_KEY = 'password-reset-required' @@ -23,17 +22,17 @@ const messages = { 'Create a password that is secure and easy to remember. Write it down or use a password manager.' } -type PasswordResetModalProps = ReturnType & - ReturnType - -const PasswordResetModal = ({ - isMobile, - needsAccountRecovery, - status, - onChangePassword -}: PasswordResetModalProps) => { +export const PasswordResetModal = () => { + const dispatch = useDispatch() + const needsAccountRecovery = useSelector(getNeedsAccountRecovery) const [showModal, setShowModal] = useState(needsAccountRecovery) const [isLoading, setIsLoading] = useState(false) + const status = useSelector(getStatus) + + const onChangePassword = (email: string, password: string) => { + dispatch(changePassword(email, password)) + } + // When the component mounts, show the modal if the reset key exists useEffect(() => { const resetRequiredEmail = window.localStorage.getItem(RESET_REQUIRED_KEY) @@ -87,32 +86,16 @@ const PasswordResetModal = ({ headerContainerClassName={styles.modalHeader} titleClassName={styles.modalTitle} > -
-
{messages.helpText}
+ + + {messages.helpText} + -
+ ) } - -function mapStateToProps(state: AppState) { - return { - needsAccountRecovery: getNeedsAccountRecovery(state), - isMobile: isMobile(), - status: getStatus(state) - } -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - onChangePassword: (email: string, password: string) => - dispatch(changePassword(email, password)) - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(PasswordResetModal) diff --git a/packages/web/src/components/sign-on/EnterPassword.module.css b/packages/web/src/components/sign-on/EnterPassword.module.css deleted file mode 100644 index 993606553d3..00000000000 --- a/packages/web/src/components/sign-on/EnterPassword.module.css +++ /dev/null @@ -1,137 +0,0 @@ -.container { - display: inline-flex; - flex-direction: column; - align-items: center; - width: 100%; - height: 100%; - user-select: none; -} - -/* Password Input */ -.passwordContainer { - display: inline-flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - width: 100%; - height: 162px; - margin-bottom: 16px; -} - -.passwordInput { - text-align: center; - height: 66px; - width: 354px; -} - -.passwordInput > input { - height: 100% !important; - width: 100% !important; - font-size: 20px !important; - color: var(--neutral); - border: 1px solid var(--neutral-light-8); - border-radius: 4px; -} - -.passwordInput :global(.placeholder) { - top: 37% !important; - left: 19px !important; - font-weight: var(--font-demi-bold) !important; -} - -.passwordInput :global(.placeholder.focus) { - font-weight: var(--font-demi-bold) !important; - transform: translate(0px, -12px) !important; -} - -.passwordInput.placeholder > input { - color: var(--neutral-light-4); -} - -.inputError > input { - margin-bottom: 13px; -} - -/* Password Requirement Icons */ -.pwdCheckContainer { - width: 354px; - margin-bottom: 24px; -} - -.statusContainer { - margin: 8px 0px; -} - -/* Continue Button */ -.continueButton .continueButtonText { - font-weight: var(--font-bold); - font-size: var(--font-l); -} - -/* Mobile Password Input */ -.isMobile .passwordContainer { - display: inline-flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - width: 100%; - height: auto; - margin-bottom: 0; -} - -.isMobile .form { - width: 100%; -} - -.isMobile .passwordInput { - text-align: center; - height: 48px; - width: 100% !important; - max-width: 354px !important; - margin-bottom: 16px; -} - -.isMobile .passwordInput > input { - height: 100% !important; - width: 100% !important; - font-size: 16px !important; - color: var(--neutral); - border: 1px solid var(--neutral-light-8); - border-radius: 4px; -} - -.isMobile input::-webkit-input-placeholder { - line-height: normal !important; -} - -.isMobile .passwordInput.placeholder > input { - color: var(--neutral-light-4); -} - -.isMobile .termsAndPrivacy { - margin: 0px 0px 16px; - font-size: var(--font-xs); - max-width: 355px; -} - -/* Mobile Password Requirement Icons */ -.isMobile .pwdCheckContainer { - width: 100% !important; - max-width: 354px !important; - margin-bottom: 16px; -} - -.isMobile .continueButton { - width: 100%; -} - -.spinner { - display: block; - height: 18px; - width: 18px; - margin: 3px; -} - -.spinner g path { - stroke: var(--white) !important; -} diff --git a/packages/web/src/components/sign-on/EnterPassword.tsx b/packages/web/src/components/sign-on/EnterPassword.tsx index 971c29303fa..47738faca69 100644 --- a/packages/web/src/components/sign-on/EnterPassword.tsx +++ b/packages/web/src/components/sign-on/EnterPassword.tsx @@ -1,278 +1,86 @@ -import { useCallback, useState } from 'react' +import { + Button, + ButtonType, + Flex, + IconArrowRight, + IconComponent, + useTheme +} from '@audius/harmony' +import { Form, Formik } from 'formik' +import { toFormikValidationSchema } from 'zod-formik-adapter' -import { Button, ButtonType, IconArrow } from '@audius/stems' -import cn from 'classnames' - -import Input from 'components/data-entry/Input' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' -import { StatusMessage } from 'components/status-message/StatusMessage' -import { commonPasswordCheck } from 'utils/commonPasswordCheck' - -import styles from './EnterPassword.module.css' +import { EnterPasswordSection } from 'pages/sign-up-page/components/EnterPasswordSection' +import { passwordSchema } from 'pages/sign-up-page/utils/passwordSchema' -const MIN_PASSWORD_LEN = 8 - -const messages = { - checks: [ - 'Must contain numbers', - 'Length must be at least 8 characters', - 'Passwords match', - 'Hard to guess' - ], - commonPwd: 'Please choose a less common password' +const initialValues = { + password: '', + confirmPassword: '' } -enum CheckState { - ERROR = 'error', - DEFAULT = 'default', - VALID = 'success' -} - -const getNumberRequirement = (password: string) => { - if (password.length === 0) return CheckState.DEFAULT - if (!/\d/.test(password)) return CheckState.ERROR - return CheckState.VALID -} - -const getLenRequirement = (password: string) => { - if (password.length === 0) return CheckState.DEFAULT - if (password.length < MIN_PASSWORD_LEN) return CheckState.ERROR - return CheckState.VALID -} - -const getMatchRequirement = ( - password: string, - passwordConfirmation: string -) => { - if (password.length === 0) return CheckState.DEFAULT - if (password !== passwordConfirmation) return CheckState.ERROR - return CheckState.VALID -} - -const getCommonPasswordCheck = async (password: string) => { - if (password.length < MIN_PASSWORD_LEN) return CheckState.DEFAULT - if (await commonPasswordCheck(password)) return CheckState.ERROR - return CheckState.VALID -} +const passwordFormikSchma = toFormikValidationSchema(passwordSchema) type EnterPasswordProps = { continueLabel: string - continueIcon?: JSX.Element - isMobile: boolean + continueIcon?: IconComponent onSubmit: (password: string) => void isLoading?: boolean } -type Requirements = { - number: CheckState - length: CheckState - match: CheckState - common: CheckState - [key: string]: CheckState -} - const EnterPassword = ({ continueLabel, continueIcon, - isMobile, - onSubmit, - isLoading + isLoading, + onSubmit }: EnterPasswordProps) => { - const [password, setPassword] = useState('') - const [passwordConfirmation, setPasswordConfirmation] = useState('') - const [requirements, setRequirements] = useState({ - number: CheckState.DEFAULT, - length: CheckState.DEFAULT, - match: CheckState.DEFAULT, - common: CheckState.DEFAULT - }) - - const onPasswordBlur = useCallback(async () => { - const commonCheck = await getCommonPasswordCheck(password) - - // When the password blurs, check if the number and length req are met - if (password) { - setRequirements((requirements) => ({ - ...requirements, - number: getNumberRequirement(password), - length: getLenRequirement(password), - common: commonCheck, - match: - passwordConfirmation === '' - ? CheckState.DEFAULT - : getMatchRequirement(password, passwordConfirmation) - })) - } - }, [password, passwordConfirmation]) - - const onPasswordConfirmationBlur = useCallback(() => { - // When the password blurs, check if the number and length req are met - if (password && passwordConfirmation) { - setRequirements((requirements) => ({ - ...requirements, - match: getMatchRequirement(password, passwordConfirmation) - })) - } - }, [password, passwordConfirmation]) - - const onPasswordChange = (password: string) => { - setPassword(password) - validatePassword() - } - - const validatePassword = useCallback(async () => { - const number = - requirements.number === CheckState.DEFAULT - ? getNumberRequirement(password) === CheckState.VALID - ? CheckState.VALID - : CheckState.DEFAULT - : getNumberRequirement(password) - const length = - requirements.length === CheckState.DEFAULT - ? getLenRequirement(password) === CheckState.VALID - ? CheckState.VALID - : CheckState.DEFAULT - : getLenRequirement(password) - const common = - requirements.common === CheckState.DEFAULT - ? (await getCommonPasswordCheck(password)) === CheckState.VALID - ? CheckState.VALID - : CheckState.DEFAULT - : await getCommonPasswordCheck(password) - setRequirements({ - ...requirements, - number, - length, - common, - match: - passwordConfirmation === '' - ? CheckState.DEFAULT - : getMatchRequirement(password, passwordConfirmation) - }) - }, [password, passwordConfirmation, requirements]) - - const onPasswordConfirmationChange = (passwordConfirmation: string) => { - if (requirements.match !== CheckState.DEFAULT) { - setRequirements({ - ...requirements, - match: - passwordConfirmation === '' - ? CheckState.DEFAULT - : getMatchRequirement(password, passwordConfirmation) - }) - } else if (password.length <= passwordConfirmation.length) { - setRequirements({ - ...requirements, - match: getMatchRequirement(password, passwordConfirmation) - }) - } - setPasswordConfirmation(passwordConfirmation) - } - - const fulfillsRequirements = () => - Object.keys(requirements).every( - (req) => requirements[req] === CheckState.VALID - ) - - const onClickSubmit = () => { - if (fulfillsRequirements()) { - onSubmit(password) - } - } - - const onConfirmKeyDown = (e: KeyboardEvent) => { - if (e.keyCode === 13 /** enter */) { - onClickSubmit() - } - } - - const pwdChecks = [ - { status: requirements.number, label: messages.checks[0] }, - { status: requirements.length, label: messages.checks[1] }, - { status: requirements.common, label: messages.checks[3] }, - { status: requirements.match, label: messages.checks[2] } - ] - const isValid = Object.keys(requirements).every( - (req) => requirements[req] === CheckState.VALID - ) - const hasError = Object.keys(requirements).some( - (req) => requirements[req] === CheckState.ERROR - ) + const Icon = continueIcon ?? IconArrowRight + const { color } = useTheme() return ( -
-
{ - e.preventDefault() - }} - autoComplete='off' - > -
- - -
-
-
- {pwdChecks.map((check, i) => ( - - ))} -
-
+ { + onSubmit(password) + }} + validationSchema={passwordFormikSchma} + > + {({ isValid }) => ( + + + + + + + )} + ) } diff --git a/packages/web/src/pages/modals/Modals.tsx b/packages/web/src/pages/modals/Modals.tsx index 89d4b52a2fa..e9c723cb675 100644 --- a/packages/web/src/pages/modals/Modals.tsx +++ b/packages/web/src/pages/modals/Modals.tsx @@ -22,7 +22,7 @@ import FirstUploadModal from 'components/first-upload-modal/FirstUploadModal' import { InboxUnavailableModal } from 'components/inbox-unavailable-modal/InboxUnavailableModal' import { LeavingAudiusModal } from 'components/leaving-audius-modal/LeavingAudiusModal' import { LockedContentModal } from 'components/locked-content-modal/LockedContentModal' -import PasswordResetModal from 'components/password-reset/PasswordResetModal' +import { PasswordResetModal } from 'components/password-reset/PasswordResetModal' import { PremiumContentPurchaseModal } from 'components/premium-content-purchase-modal/PremiumContentPurchaseModal' import ShareSoundToTikTokModal from 'components/share-sound-to-tiktok-modal/ShareSoundToTikTokModal' import { TipAudioModal } from 'components/tipping/tip-audio/TipAudioModal' diff --git a/packages/web/src/pages/sign-in-page/SignInPage.tsx b/packages/web/src/pages/sign-in-page/SignInPage.tsx index a685cb4bd18..d64703b6bf1 100644 --- a/packages/web/src/pages/sign-in-page/SignInPage.tsx +++ b/packages/web/src/pages/sign-in-page/SignInPage.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useState } from 'react' import { Flex, @@ -21,6 +21,7 @@ import { HarmonyPasswordField } from 'components/form-fields/HarmonyPasswordFiel import { HarmonyTextField } from 'components/form-fields/HarmonyTextField' import PreloadImage from 'components/preload-image/PreloadImage' import { useMedia } from 'hooks/useMedia' +import { ForgotPasswordHelper } from 'pages/sign-on/components/desktop/ForgotPasswordHelper' import { Heading } from 'pages/sign-up-page/components/layout' import { useSelector } from 'utils/reducer' import { SIGN_UP_PAGE } from 'utils/route' @@ -53,6 +54,7 @@ const initialValues = { export const SignInPage = () => { const dispatch = useDispatch() const { isMobile } = useMedia() + const [showForgotPassword, setShowForgotPassword] = useState(false) const signInStatus = useSelector(getStatus) @@ -116,6 +118,9 @@ export const SignInPage = () => { variant='visible' textVariant='body' css={{ textAlign: isMobile ? 'center' : undefined }} + onClick={() => { + setShowForgotPassword(true) + }} > {messages.forgotPassword} @@ -128,6 +133,10 @@ export const SignInPage = () => { ) : null} + setShowForgotPassword(false)} + /> ) } diff --git a/packages/web/src/pages/sign-on/components/desktop/ForgotPasswordHelper.module.css b/packages/web/src/pages/sign-on/components/desktop/ForgotPasswordHelper.module.css index 49eda36a9b9..44e93f92afd 100644 --- a/packages/web/src/pages/sign-on/components/desktop/ForgotPasswordHelper.module.css +++ b/packages/web/src/pages/sign-on/components/desktop/ForgotPasswordHelper.module.css @@ -1,57 +1,3 @@ -.questionButtonIcon { - margin-right: 10px; -} - -.modalWidth { +.modal { width: 510px; } - -.modalBody { - margin: 24px; -} - -.restoreAccessText { - text-align: center; - font-size: var(--font-m); - line-height: 24px; - font-weight: var(--font-demi-bold); - margin-bottom: 24px; -} - -.emailContainer { - display: flex; - flex-direction: row; - padding: 12px 24px; - border: 1px solid #dad9e0; - box-sizing: border-box; - border-radius: 8px; - font-size: 16px; - line-height: 20px; - column-gap: 12px; -} - -.emailSubheadings { - display: flex; - flex-direction: column; - width: 64px; - color: var(--neutral-light-4); - font-weight: var(--font-bold); - row-gap: 12px; -} - -.emailText { - display: flex; - flex-direction: column; - flex-grow: 1; - color: var(--neutral); - font-weight: var(--font-demi-bold); - row-gap: 12px; -} - -.emailBody { - cursor: pointer; -} - -.emailBody:hover { - color: var(--secondary); -} diff --git a/packages/web/src/pages/sign-on/components/desktop/ForgotPasswordHelper.tsx b/packages/web/src/pages/sign-on/components/desktop/ForgotPasswordHelper.tsx index ad1e524c745..b925942f342 100644 --- a/packages/web/src/pages/sign-on/components/desktop/ForgotPasswordHelper.tsx +++ b/packages/web/src/pages/sign-on/components/desktop/ForgotPasswordHelper.tsx @@ -1,4 +1,6 @@ +import { Flex, Text, useTheme } from '@audius/harmony' import { Modal } from '@audius/stems' +import { CSSObject } from '@emotion/styled' import IconQuestionMark from 'assets/img/iconQuestionMark.svg' import Toast from 'components/toast/Toast' @@ -29,12 +31,14 @@ export const ForgotPasswordHelper = ({ isOpen, onClose }: ForgotPasswordHelperProps) => { + const { color } = useTheme() + const renderTitle = () => { return ( - <> - -
{messages.forgotPassword}
- + + + {messages.forgotPassword} + ) } @@ -46,6 +50,13 @@ export const ForgotPasswordHelper = ({ copyToClipboard(messages.subject) } + const linkCss: CSSObject = { + cursor: 'pointer', + ':hover': { + color: color.secondary.secondary + } + } + return ( -
-
{messages.restoreAccess}
-
-
-
{messages.fromHeader}
-
{messages.subjectHeader}
-
-
+ + + {messages.restoreAccess} + + + + {messages.fromHeader} + {messages.subjectHeader} + + -
+ {messages.from} -
+
-
+ {messages.subject} -
+
-
-
-
+ + +
) } diff --git a/packages/web/src/pages/sign-up-page/components/EnterPasswordSection.tsx b/packages/web/src/pages/sign-up-page/components/EnterPasswordSection.tsx new file mode 100644 index 00000000000..d489912c560 --- /dev/null +++ b/packages/web/src/pages/sign-up-page/components/EnterPasswordSection.tsx @@ -0,0 +1,23 @@ +import { Flex } from '@audius/harmony' + +import { PasswordField } from 'components/form-fields/PasswordField' + +import { CompletionChecklist } from './CompletionChecklist' + +const messages = { + passwordLabel: 'Password', + confirmPasswordLabel: 'Confirm Password' +} + +export const EnterPasswordSection = () => { + return ( + + + + + + ) +} diff --git a/packages/web/src/pages/sign-up-page/pages/CreatePasswordPage.tsx b/packages/web/src/pages/sign-up-page/pages/CreatePasswordPage.tsx index 149fa841183..242cae392cf 100644 --- a/packages/web/src/pages/sign-up-page/pages/CreatePasswordPage.tsx +++ b/packages/web/src/pages/sign-up-page/pages/CreatePasswordPage.tsx @@ -7,12 +7,11 @@ import { toFormikValidationSchema } from 'zod-formik-adapter' import { setValueField } from 'common/store/pages/signon/actions' import { getEmailField } from 'common/store/pages/signon/selectors' -import { PasswordField } from 'components/form-fields/PasswordField' import { useMedia } from 'hooks/useMedia' import { useNavigateToPage } from 'hooks/useNavigateToPage' import { SIGN_UP_HANDLE_PAGE } from 'utils/route' -import { CompletionChecklist } from '../components/CompletionChecklist' +import { EnterPasswordSection } from '../components/EnterPasswordSection' import { SignUpAgreementText } from '../components/SignUpPolicyText' import { Heading, Page, PageFooter, ReadOnlyField } from '../components/layout' import { passwordSchema } from '../utils/passwordSchema' @@ -21,9 +20,7 @@ const messages = { createYourPassword: 'Create Your Password', description: 'Create a password that’s secure and easy to remember! We can’t reset your password, so write it down or use a password manager.', - yourEmail: 'Your Email', - passwordLabel: 'Password', - confirmPasswordLabel: 'Confirm Password' + yourEmail: 'Your Email' } const initialValues = { @@ -70,12 +67,7 @@ export const CreatePasswordPage = () => { label={messages.yourEmail} value={emailField.value} /> - - - +