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