diff --git a/src/constants/regex.ts b/src/constants/regex.ts index f703ae7a..b49e7ce8 100644 --- a/src/constants/regex.ts +++ b/src/constants/regex.ts @@ -2,4 +2,6 @@ export const REGEX = { EMAIL: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/g, // PASSWORD - minimum: at least 8 chars, 1 small letter, 1 big letter, 1 special char, 1 number REGISTRATION_PASSWORD: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@#$%^&*!])[A-Za-z\d@#$%^&*!]{8,}$/, + MIN_8_CHARS: /^.{8,}$/, + SPECIAL_CHAR: /.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?].*/, } as const diff --git a/src/hooks/usePasswordValidation.tsx b/src/hooks/usePasswordValidation.tsx new file mode 100644 index 00000000..179528de --- /dev/null +++ b/src/hooks/usePasswordValidation.tsx @@ -0,0 +1,59 @@ +import { REGEX } from '@baca/constants' +import { Box, Icon, Row, Text } from '@baca/design-system' +import { IconNames } from '@baca/types/icon' +import { useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' + +// make sure to add proper translations to path `form.validation.password_` +const passwordSuggestionsList = ['min_8_chars', 'min_1_special_char'] as const + +export const usePasswordValidation = () => { + const { t } = useTranslation() + const [passwordErrors, setPasswordErrors] = useState< + ('min_8_chars' | 'min_1_special_char' | '')[] + >([]) + const [isPasswordError, setIsPasswordError] = useState(false) + const [showValidationState, setShowValidationState] = useState(false) + + const validationFn = useCallback( + (password: string) => { + const min8Chars = !REGEX.MIN_8_CHARS.test(password) ? 'min_8_chars' : '' + const min1SpecialChar = !REGEX.SPECIAL_CHAR.test(password) ? 'min_1_special_char' : '' + + !showValidationState && setShowValidationState(true) + setIsPasswordError(!!min8Chars || !!min1SpecialChar) + setPasswordErrors([min8Chars, min1SpecialChar]) + return !!min8Chars || !!min1SpecialChar ? 'Error' : false + }, + [showValidationState] + ) + + const passwordSuggestions = useMemo(() => { + return passwordSuggestionsList.map((suggestion) => { + const isError = passwordErrors?.includes(suggestion) + const iconName: IconNames = isError ? 'close-line' : 'check-line' + const iconColor: ColorNames = !showValidationState + ? 'fg.disabled_subtle' + : isError + ? 'utility.error.500' + : 'utility.success.500' + + return ( + + + + + + {t(`form.validation.password_${suggestion}`)} + + + ) + }) + }, [passwordErrors, showValidationState, t]) + + return { + isPasswordError, + passwordSuggestions, + validationFn, + } +} diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 02f093b7..9bb2ec56 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -48,7 +48,8 @@ "select": {}, "validation": { "invalid_email_format": "Incorrect e-mail address format", - "invalid_password_format": "Password must be at least 8 characters long, contain at least one uppercase letter, one lowercase letter, one number, and one special character", + "password_min_1_special_char": "Must contain one special character ", + "password_min_8_chars": "Must be at least 8 characters", "passwords_does_not_match": "Passwords does not match", "required": "This field is required" } diff --git a/src/i18n/translations/pl.json b/src/i18n/translations/pl.json index 1137b946..3ed47ae2 100644 --- a/src/i18n/translations/pl.json +++ b/src/i18n/translations/pl.json @@ -48,7 +48,8 @@ "select": {}, "validation": { "invalid_email_format": "Niepoprawny format adresu e-mail", - "invalid_password_format": "Hasło musi zawierać minimum: 8 znaków, jedną wielką literę, jedną małą literę, jedną cyfrę i jeden znak specjalny", + "password_min_1_special_char": "Musi zawierać co najmniej 1 znaj specjalny", + "password_min_8_chars": "Musi składać się z co najmniej 8 znaków", "passwords_does_not_match": "Wprowadzone hasła nie są identyczne", "required": "Pole wymagane" } diff --git a/src/screens/auth/ResetPasswordScreen.tsx b/src/screens/auth/ResetPasswordScreen.tsx index 978657c2..e710c9b4 100644 --- a/src/screens/auth/ResetPasswordScreen.tsx +++ b/src/screens/auth/ResetPasswordScreen.tsx @@ -1,7 +1,7 @@ import { CompanyLogo, ControlledField, FeaturedIcon, FormWrapper } from '@baca/components' -import { REGEX } from '@baca/constants' import { Button, Center, Display, Spacer, Text } from '@baca/design-system' import { useEffect, useResetPasswordForm, useTranslation } from '@baca/hooks' +import { usePasswordValidation } from '@baca/hooks/usePasswordValidation' import { router, useLocalSearchParams } from 'expo-router' const navigateToLogin = () => { @@ -14,6 +14,8 @@ export const ResetPasswordScreen = () => { const { control, errors, isSubmitting, reset, submit } = useResetPasswordForm() + const { isPasswordError, passwordSuggestions, validationFn } = usePasswordValidation() + useEffect(() => { if (hash) { reset({ hash }) @@ -36,25 +38,22 @@ export const ResetPasswordScreen = () => { + {passwordSuggestions} { router.navigate('/sign-in') } @@ -14,6 +16,8 @@ export const SignUpScreen = () => { const { control, errors, register, isSubmitting, setFocus } = useSignUpForm() + const { isPasswordError, passwordSuggestions, validationFn } = usePasswordValidation() + const focusLastNameInput = useCallback(() => setFocus('lastName'), [setFocus]) const focusEmailInput = useCallback(() => setFocus('email'), [setFocus]) const focusPasswordInput = useCallback(() => setFocus('password'), [setFocus]) @@ -74,19 +78,20 @@ export const SignUpScreen = () => { }} /> + {passwordSuggestions}