From 08f114a15c0293519d240f0acbd02d47c60d6187 Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 9 Jun 2022 11:03:12 -0400 Subject: [PATCH 1/6] min chars --- .../features/verification/registration.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/verification/registration.tsx b/app/soapbox/features/verification/registration.tsx index 8b76dce34..68a4805db 100644 --- a/app/soapbox/features/verification/registration.tsx +++ b/app/soapbox/features/verification/registration.tsx @@ -1,4 +1,5 @@ import { AxiosError } from 'axios'; +import classNames from 'classnames'; import * as React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; @@ -9,7 +10,7 @@ import { fetchInstance } from 'soapbox/actions/instance'; import { startOnboarding } from 'soapbox/actions/onboarding'; import snackbar from 'soapbox/actions/snackbar'; import { createAccount, removeStoredVerification } from 'soapbox/actions/verification'; -import { Button, Form, FormGroup, Input } from 'soapbox/components/ui'; +import { Button, Form, FormGroup, HStack, Icon, Input, Stack, Text } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; import { getRedirectUrl } from 'soapbox/utils/redirect'; @@ -44,6 +45,8 @@ const Registration = () => { const [shouldRedirect, setShouldRedirect] = React.useState(false); const { username, password } = state; + const meetsLengthRequirements = React.useMemo(() => password.length >= 8, [password]); + const handleSubmit = React.useCallback((event) => { event.preventDefault(); @@ -119,6 +122,21 @@ const Registration = () => { onChange={handleInputChange} required /> + + + + + + 8 characters + +
From 4fc43afe1b557d93a00b9e5bbd01f2f0576b82bf Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 9 Jun 2022 13:45:09 -0400 Subject: [PATCH 2/6] Add new ValidationCheckmark component --- .../__tests__/validation-checkmark.test.tsx | 29 +++++++++++++++++++ .../components/validation-checkmark.tsx | 28 ++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 app/soapbox/components/__tests__/validation-checkmark.test.tsx create mode 100644 app/soapbox/components/validation-checkmark.tsx diff --git a/app/soapbox/components/__tests__/validation-checkmark.test.tsx b/app/soapbox/components/__tests__/validation-checkmark.test.tsx new file mode 100644 index 000000000..d3a02c718 --- /dev/null +++ b/app/soapbox/components/__tests__/validation-checkmark.test.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { render, screen } from '../../jest/test-helpers'; +import ValidationCheckmark from '../validation-checkmark'; + +describe('', () => { + it('renders text', () => { + const text = 'some validation'; + render(); + + expect(screen.getByTestId('validation-checkmark')).toHaveTextContent(text); + }); + + it('uses a green check when valid', () => { + const text = 'some validation'; + render(); + + expect(screen.getByTestId('svg-icon-loader')).toHaveClass('text-success-500'); + expect(screen.getByTestId('svg-icon-loader')).not.toHaveClass('text-gray-500'); + }); + + it('uses a gray check when valid', () => { + const text = 'some validation'; + render(); + + expect(screen.getByTestId('svg-icon-loader')).toHaveClass('text-gray-500'); + expect(screen.getByTestId('svg-icon-loader')).not.toHaveClass('text-success-500'); + }); +}); diff --git a/app/soapbox/components/validation-checkmark.tsx b/app/soapbox/components/validation-checkmark.tsx new file mode 100644 index 000000000..0c0ab42d6 --- /dev/null +++ b/app/soapbox/components/validation-checkmark.tsx @@ -0,0 +1,28 @@ +import classNames from 'classnames'; +import React from 'react'; + +import { HStack, Icon, Text } from 'soapbox/components/ui'; + +interface IValidationCheckmark { + isValid: boolean + text: string +} + +const ValidationCheckmark = ({ isValid, text }: IValidationCheckmark) => { + return ( + + + + {text} + + ); +}; + +export default ValidationCheckmark; From cf128d70b4def1dfc0310221f6524fb9e6689732 Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 9 Jun 2022 13:45:23 -0400 Subject: [PATCH 3/6] Apply new ValidationCheckmark component to Registration --- .../__tests__/registration.test.tsx | 17 +++++ .../features/verification/registration.tsx | 69 ++++++++++++++----- app/soapbox/locales/en.json | 3 + 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/app/soapbox/features/verification/__tests__/registration.test.tsx b/app/soapbox/features/verification/__tests__/registration.test.tsx index ff5da7e97..f82c0dab4 100644 --- a/app/soapbox/features/verification/__tests__/registration.test.tsx +++ b/app/soapbox/features/verification/__tests__/registration.test.tsx @@ -64,4 +64,21 @@ describe('', () => { expect(screen.getByTestId('toast')).toHaveTextContent(/failed to register your account/i); }); }); + + describe('validations', () => { + it('should undisable button with valid password', async() => { + render(); + + expect(screen.getByTestId('button')).toBeDisabled(); + fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'Password' } }); + expect(screen.getByTestId('button')).not.toBeDisabled(); + }); + + it('should disable button with invalid password', async() => { + render(); + + fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'Passwor' } }); + expect(screen.getByTestId('button')).toBeDisabled(); + }); + }); }); diff --git a/app/soapbox/features/verification/registration.tsx b/app/soapbox/features/verification/registration.tsx index 68a4805db..567db8b16 100644 --- a/app/soapbox/features/verification/registration.tsx +++ b/app/soapbox/features/verification/registration.tsx @@ -1,5 +1,4 @@ import { AxiosError } from 'axios'; -import classNames from 'classnames'; import * as React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; @@ -10,7 +9,8 @@ import { fetchInstance } from 'soapbox/actions/instance'; import { startOnboarding } from 'soapbox/actions/onboarding'; import snackbar from 'soapbox/actions/snackbar'; import { createAccount, removeStoredVerification } from 'soapbox/actions/verification'; -import { Button, Form, FormGroup, HStack, Icon, Input, Stack, Text } from 'soapbox/components/ui'; +import { Button, Form, FormGroup, Input, Stack } from 'soapbox/components/ui'; +import ValidationCheckmark from 'soapbox/components/validation-checkmark'; import { useAppSelector } from 'soapbox/hooks'; import { getRedirectUrl } from 'soapbox/utils/redirect'; @@ -27,6 +27,18 @@ const messages = defineMessages({ id: 'registrations.error', defaultMessage: 'Failed to register your account.', }, + minimumCharacters: { + id: 'registration.validation.minimum_characters', + defaultMessage: '8 characters', + }, + capitalLetter: { + id: 'registration.validation.capital_letter', + defaultMessage: '1 capital letter', + }, + lowercaseLetter: { + id: 'registration.validation.lowercase_letter', + defaultMessage: '1 lowercase letter', + }, }); const initialState = { @@ -34,6 +46,19 @@ const initialState = { password: '', }; +const hasUppercaseCharacter = (string: string) => { + for (let i = 0; i < string.length; i++) { + if (string.charAt(i) === string.charAt(i).toUpperCase() && string.charAt(i).match(/[a-z]/i)) { + return true; + } + } + return false; +}; + +const hasLowercaseCharacter = (string: string) => { + return string.toUpperCase() !== string; +}; + const Registration = () => { const dispatch = useDispatch(); const intl = useIntl(); @@ -46,11 +71,13 @@ const Registration = () => { const { username, password } = state; const meetsLengthRequirements = React.useMemo(() => password.length >= 8, [password]); + const meetsCapitalLetterRequirements = React.useMemo(() => hasUppercaseCharacter(password), [password]); + const meetsLowercaseLetterRequirements = React.useMemo(() => hasLowercaseCharacter(password), [password]); + const hasValidPassword = meetsLengthRequirements && meetsCapitalLetterRequirements && meetsLowercaseLetterRequirements; const handleSubmit = React.useCallback((event) => { event.preventDefault(); - // TODO: handle validation errors from Pepe dispatch(createAccount(username, password)) .then(() => dispatch(logIn(intl, username, password))) .then(({ access_token }: any) => dispatch(verifyCredentials(access_token))) @@ -121,26 +148,36 @@ const Registration = () => { value={password} onChange={handleInputChange} required + data-testid='password-input' /> - - - + + - 8 characters - + + +
- +
diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 9f3894ac5..d6808069a 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -833,6 +833,9 @@ "registration.sign_up": "Sign up", "registration.tos": "Terms of Service", "registration.username_unavailable": "Username is already taken.", + "registration.validation.minimum_characters": "8 characters", + "registration.validation.capital_letter": "1 capital letter", + "registration.validation.lowercase_letter": "1 lowercase letter", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "now", From a8b738a719c372f70c76b7cdd454edfab7f6a14f Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 9 Jun 2022 15:51:50 -0400 Subject: [PATCH 4/6] Add to other Password inputs --- .../components/validation-checkmark.tsx | 4 +- .../components/password_reset_confirm.tsx | 11 ++- app/soapbox/features/edit_password/index.tsx | 12 +++- .../components/password-indicator.tsx | 72 +++++++++++++++++++ .../features/verification/registration.tsx | 50 ++----------- app/soapbox/utils/features.ts | 8 +++ 6 files changed, 105 insertions(+), 52 deletions(-) create mode 100644 app/soapbox/features/verification/components/password-indicator.tsx diff --git a/app/soapbox/components/validation-checkmark.tsx b/app/soapbox/components/validation-checkmark.tsx index 0c0ab42d6..111066ccf 100644 --- a/app/soapbox/components/validation-checkmark.tsx +++ b/app/soapbox/components/validation-checkmark.tsx @@ -12,10 +12,10 @@ const ValidationCheckmark = ({ isValid, text }: IValidationCheckmark) => { return ( diff --git a/app/soapbox/features/auth_login/components/password_reset_confirm.tsx b/app/soapbox/features/auth_login/components/password_reset_confirm.tsx index 2f30e700f..a7709834f 100644 --- a/app/soapbox/features/auth_login/components/password_reset_confirm.tsx +++ b/app/soapbox/features/auth_login/components/password_reset_confirm.tsx @@ -4,7 +4,8 @@ import { Redirect } from 'react-router-dom'; import { resetPasswordConfirm } from 'soapbox/actions/security'; import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; -import { useAppDispatch } from 'soapbox/hooks'; +import PasswordIndicator from 'soapbox/features/verification/components/password-indicator'; +import { useAppDispatch, useFeatures } from 'soapbox/hooks'; const token = new URLSearchParams(window.location.search).get('reset_password_token'); @@ -22,9 +23,11 @@ const Statuses = { const PasswordResetConfirm = () => { const intl = useIntl(); const dispatch = useAppDispatch(); + const { passwordRequirements } = useFeatures(); const [password, setPassword] = React.useState(''); const [status, setStatus] = React.useState(Statuses.IDLE); + const [hasValidPassword, setHasValidPassword] = React.useState(passwordRequirements ? false : true); const isLoading = status === Statuses.LOADING; @@ -71,10 +74,14 @@ const PasswordResetConfirm = () => { onChange={onChange} required /> + + {passwordRequirements && ( + + )} - diff --git a/app/soapbox/features/edit_password/index.tsx b/app/soapbox/features/edit_password/index.tsx index e95e6dec4..5cebc3c98 100644 --- a/app/soapbox/features/edit_password/index.tsx +++ b/app/soapbox/features/edit_password/index.tsx @@ -4,7 +4,9 @@ import { defineMessages, useIntl } from 'react-intl'; import { changePassword } from 'soapbox/actions/security'; import snackbar from 'soapbox/actions/snackbar'; import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppDispatch, useFeatures } from 'soapbox/hooks'; + +import PasswordIndicator from '../verification/components/password-indicator'; const messages = defineMessages({ updatePasswordSuccess: { id: 'security.update_password.success', defaultMessage: 'Password successfully updated.' }, @@ -22,9 +24,11 @@ const initialState = { currentPassword: '', newPassword: '', newPasswordConfirma const EditPassword = () => { const intl = useIntl(); const dispatch = useAppDispatch(); + const { passwordRequirements } = useFeatures(); const [state, setState] = React.useState(initialState); const [isLoading, setLoading] = React.useState(false); + const [hasValidPassword, setHasValidPassword] = React.useState(passwordRequirements ? false : true); const { currentPassword, newPassword, newPasswordConfirmation } = state; @@ -75,6 +79,10 @@ const EditPassword = () => { onChange={handleInputChange} value={newPassword} /> + + {passwordRequirements && ( + + )} @@ -91,7 +99,7 @@ const EditPassword = () => { {intl.formatMessage(messages.cancel)} - diff --git a/app/soapbox/features/verification/components/password-indicator.tsx b/app/soapbox/features/verification/components/password-indicator.tsx new file mode 100644 index 000000000..7b804d3d6 --- /dev/null +++ b/app/soapbox/features/verification/components/password-indicator.tsx @@ -0,0 +1,72 @@ +import React, { useEffect, useMemo } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { Stack } from 'soapbox/components/ui'; +import ValidationCheckmark from 'soapbox/components/validation-checkmark'; + +const messages = defineMessages({ + minimumCharacters: { + id: 'registration.validation.minimum_characters', + defaultMessage: '8 characters', + }, + capitalLetter: { + id: 'registration.validation.capital_letter', + defaultMessage: '1 capital letter', + }, + lowercaseLetter: { + id: 'registration.validation.lowercase_letter', + defaultMessage: '1 lowercase letter', + }, +}); + +const hasUppercaseCharacter = (string: string) => { + for (let i = 0; i < string.length; i++) { + if (string.charAt(i) === string.charAt(i).toUpperCase() && string.charAt(i).match(/[a-z]/i)) { + return true; + } + } + return false; +}; + +const hasLowercaseCharacter = (string: string) => { + return string.toUpperCase() !== string; +}; + +interface IPasswordIndicator { + onChange(isValid: boolean): void + password: string +} + +const PasswordIndicator = ({ onChange, password }: IPasswordIndicator) => { + const intl = useIntl(); + + const meetsLengthRequirements = useMemo(() => password.length >= 8, [password]); + const meetsCapitalLetterRequirements = useMemo(() => hasUppercaseCharacter(password), [password]); + const meetsLowercaseLetterRequirements = useMemo(() => hasLowercaseCharacter(password), [password]); + const hasValidPassword = meetsLengthRequirements && meetsCapitalLetterRequirements && meetsLowercaseLetterRequirements; + + useEffect(() => { + onChange(hasValidPassword); + }, [hasValidPassword]); + + return ( + + + + + + + + ); +}; + +export default PasswordIndicator; diff --git a/app/soapbox/features/verification/registration.tsx b/app/soapbox/features/verification/registration.tsx index 567db8b16..923badd61 100644 --- a/app/soapbox/features/verification/registration.tsx +++ b/app/soapbox/features/verification/registration.tsx @@ -14,6 +14,8 @@ import ValidationCheckmark from 'soapbox/components/validation-checkmark'; import { useAppSelector } from 'soapbox/hooks'; import { getRedirectUrl } from 'soapbox/utils/redirect'; +import PasswordIndicator from './components/password-indicator'; + const messages = defineMessages({ success: { id: 'registrations.success', @@ -27,18 +29,6 @@ const messages = defineMessages({ id: 'registrations.error', defaultMessage: 'Failed to register your account.', }, - minimumCharacters: { - id: 'registration.validation.minimum_characters', - defaultMessage: '8 characters', - }, - capitalLetter: { - id: 'registration.validation.capital_letter', - defaultMessage: '1 capital letter', - }, - lowercaseLetter: { - id: 'registration.validation.lowercase_letter', - defaultMessage: '1 lowercase letter', - }, }); const initialState = { @@ -46,19 +36,6 @@ const initialState = { password: '', }; -const hasUppercaseCharacter = (string: string) => { - for (let i = 0; i < string.length; i++) { - if (string.charAt(i) === string.charAt(i).toUpperCase() && string.charAt(i).match(/[a-z]/i)) { - return true; - } - } - return false; -}; - -const hasLowercaseCharacter = (string: string) => { - return string.toUpperCase() !== string; -}; - const Registration = () => { const dispatch = useDispatch(); const intl = useIntl(); @@ -68,13 +45,9 @@ const Registration = () => { const [state, setState] = React.useState(initialState); const [shouldRedirect, setShouldRedirect] = React.useState(false); + const [hasValidPassword, setHasValidPassword] = React.useState(false); const { username, password } = state; - const meetsLengthRequirements = React.useMemo(() => password.length >= 8, [password]); - const meetsCapitalLetterRequirements = React.useMemo(() => hasUppercaseCharacter(password), [password]); - const meetsLowercaseLetterRequirements = React.useMemo(() => hasLowercaseCharacter(password), [password]); - const hasValidPassword = meetsLengthRequirements && meetsCapitalLetterRequirements && meetsLowercaseLetterRequirements; - const handleSubmit = React.useCallback((event) => { event.preventDefault(); @@ -151,22 +124,7 @@ const Registration = () => { data-testid='password-input' /> - - - - - - - +
diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 891cdd621..8f8a65cc1 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -380,6 +380,14 @@ const getInstanceFeatures = (instance: Instance) => { */ paginatedContext: v.software === TRUTHSOCIAL, + /** + * Require minimum password requirements. + * - 8 characters + * - 1 uppercase + * - 1 lowercase + */ + passwordRequirements: v.software === TRUTHSOCIAL, + /** * Displays a form to follow a user when logged out. * @see POST /main/ostatus From 1b88f2f36e29a5622e23f9fc05ae8c4c5eff011b Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 9 Jun 2022 15:53:03 -0400 Subject: [PATCH 5/6] Fix test --- .../components/__tests__/validation-checkmark.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/__tests__/validation-checkmark.test.tsx b/app/soapbox/components/__tests__/validation-checkmark.test.tsx index d3a02c718..c9e204a6e 100644 --- a/app/soapbox/components/__tests__/validation-checkmark.test.tsx +++ b/app/soapbox/components/__tests__/validation-checkmark.test.tsx @@ -16,14 +16,14 @@ describe('', () => { render(); expect(screen.getByTestId('svg-icon-loader')).toHaveClass('text-success-500'); - expect(screen.getByTestId('svg-icon-loader')).not.toHaveClass('text-gray-500'); + expect(screen.getByTestId('svg-icon-loader')).not.toHaveClass('text-gray-400'); }); it('uses a gray check when valid', () => { const text = 'some validation'; render(); - expect(screen.getByTestId('svg-icon-loader')).toHaveClass('text-gray-500'); + expect(screen.getByTestId('svg-icon-loader')).toHaveClass('text-gray-400'); expect(screen.getByTestId('svg-icon-loader')).not.toHaveClass('text-success-500'); }); }); From 56715757b8a2ea8156dc35cf3dc5e82238720146 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 10 Jun 2022 10:34:35 -0400 Subject: [PATCH 6/6] Lint --- app/soapbox/features/verification/registration.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/soapbox/features/verification/registration.tsx b/app/soapbox/features/verification/registration.tsx index 923badd61..c2c09fbce 100644 --- a/app/soapbox/features/verification/registration.tsx +++ b/app/soapbox/features/verification/registration.tsx @@ -9,8 +9,7 @@ import { fetchInstance } from 'soapbox/actions/instance'; import { startOnboarding } from 'soapbox/actions/onboarding'; import snackbar from 'soapbox/actions/snackbar'; import { createAccount, removeStoredVerification } from 'soapbox/actions/verification'; -import { Button, Form, FormGroup, Input, Stack } from 'soapbox/components/ui'; -import ValidationCheckmark from 'soapbox/components/validation-checkmark'; +import { Button, Form, FormGroup, Input } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; import { getRedirectUrl } from 'soapbox/utils/redirect';