From 05cc7c10d47c0defc84f7b63e66150de8ac41ac4 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Wed, 11 Dec 2024 00:46:01 -0300 Subject: [PATCH] chore: created CustomFields component, useVerifyPassword and useCustomFields --- app/containers/CustomFields/index.tsx | 76 ++++++++++++ app/lib/constants/defaultSettings.ts | 27 +++++ app/lib/hooks/useCustomFields.ts | 20 ++++ app/lib/hooks/useVerifyPassword.ts | 112 ++++++++++++++++++ app/views/RegisterView/PasswordTips.tsx | 33 +++--- app/views/RegisterView/index.tsx | 94 +++------------ .../methods/getParsedCustomFields.ts | 15 --- 7 files changed, 267 insertions(+), 110 deletions(-) create mode 100644 app/containers/CustomFields/index.tsx create mode 100644 app/lib/hooks/useCustomFields.ts create mode 100644 app/lib/hooks/useVerifyPassword.ts delete mode 100644 app/views/RegisterView/methods/getParsedCustomFields.ts diff --git a/app/containers/CustomFields/index.tsx b/app/containers/CustomFields/index.tsx new file mode 100644 index 0000000000..3f5cbc9142 --- /dev/null +++ b/app/containers/CustomFields/index.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import RNPickerSelect from 'react-native-picker-select'; + +import { FormTextInput } from '../TextInput'; +import useParsedCustomFields from '../../lib/hooks/useCustomFields'; + +interface ICustomFields { + Accounts_CustomFields: string; + customFields: any; + onCustomFieldChange: (value: any) => void; +} + +const CustomFields = ({ Accounts_CustomFields, customFields, onCustomFieldChange }: ICustomFields) => { + if (!Accounts_CustomFields) { + return null; + } + const { parsedCustomFields } = useParsedCustomFields(Accounts_CustomFields); + try { + return Object.keys(parsedCustomFields).map((key: string, index: number, array: any) => { + if (parsedCustomFields[key].type === 'select') { + const options = parsedCustomFields[key].options.map((option: string) => ({ label: option, value: option })); + return ( + { + const newValue: { [key: string]: string } = {}; + newValue[key] = value; + onCustomFieldChange({ ...customFields, ...newValue }); + }} + value={customFields[key]}> + { + // @ts-ignore + this[key] = e; + }} + label={key} + placeholder={key} + value={customFields[key]} + testID='settings-view-language' + /> + + ); + } + + return ( + { + // @ts-ignore + this[key] = e; + }} + key={key} + label={key} + placeholder={key} + value={customFields[key]} + onChangeText={value => { + const newValue: { [key: string]: string } = {}; + newValue[key] = value; + onCustomFieldChange({ ...customFields, ...newValue }); + }} + onSubmitEditing={() => { + if (array.length - 1 > index) { + // @ts-ignore + return this[array[index + 1]].focus(); + } + }} + containerStyle={{ marginBottom: 0, marginTop: 0 }} + /> + ); + }); + } catch (error) { + return null; + } +}; + +export default CustomFields; diff --git a/app/lib/constants/defaultSettings.ts b/app/lib/constants/defaultSettings.ts index 82f321a073..60677386aa 100644 --- a/app/lib/constants/defaultSettings.ts +++ b/app/lib/constants/defaultSettings.ts @@ -69,6 +69,33 @@ export const defaultSettings = { Accounts_PasswordReset: { type: 'valueAsBoolean' }, + Accounts_Password_Policy_Enabled: { + type: 'valueAsBoolean' + }, + Accounts_Password_Policy_MinLength: { + type: 'valueAsNumber' + }, + Accounts_Password_Policy_MaxLength: { + type: 'valueAsNumber' + }, + Accounts_Password_Policy_ForbidRepeatingCharacters: { + type: 'valueAsBoolean' + }, + Accounts_Password_Policy_ForbidRepeatingCharactersCount: { + type: 'valueAsNumber' + }, + Accounts_Password_Policy_AtLeastOneLowercase: { + type: 'valueAsBoolean' + }, + Accounts_Password_Policy_AtLeastOneUppercase: { + type: 'valueAsBoolean' + }, + Accounts_Password_Policy_AtLeastOneNumber: { + type: 'valueAsBoolean' + }, + Accounts_Password_Policy_AtLeastOneSpecialCharacter: { + type: 'valueAsBoolean' + }, Accounts_RegistrationForm: { type: 'valueAsString' }, diff --git a/app/lib/hooks/useCustomFields.ts b/app/lib/hooks/useCustomFields.ts new file mode 100644 index 0000000000..e54179bad9 --- /dev/null +++ b/app/lib/hooks/useCustomFields.ts @@ -0,0 +1,20 @@ +import { useMemo } from 'react'; +import log from '../../lib/methods/helpers/log'; + +const useParsedCustomFields = (Accounts_CustomFields: string) => { + const parsedCustomFields = useMemo(() => { + let parsed: any = {}; + if (Accounts_CustomFields) { + try { + parsed = JSON.parse(Accounts_CustomFields); + } catch (error) { + log(error); + } + } + return parsed; + }, [Accounts_CustomFields]); + + return { parsedCustomFields }; +}; + +export default useParsedCustomFields; diff --git a/app/lib/hooks/useVerifyPassword.ts b/app/lib/hooks/useVerifyPassword.ts new file mode 100644 index 0000000000..d092f31212 --- /dev/null +++ b/app/lib/hooks/useVerifyPassword.ts @@ -0,0 +1,112 @@ +import { useMemo } from 'react'; + +import { useSetting } from './useSetting'; +import i18n from '../../i18n'; + +export interface IPasswordPolicy { + name: string; + label: string; + regex: RegExp; +} + +const useVerifyPassword = (password: string, confirmPassword: string) => { + const Accounts_Password_Policy_AtLeastOneLowercase = useSetting('Accounts_Password_Policy_AtLeastOneLowercase'); + const Accounts_Password_Policy_Enabled = useSetting('Accounts_Password_Policy_Enabled'); + const Accounts_Password_Policy_AtLeastOneNumber = useSetting('Accounts_Password_Policy_AtLeastOneNumber'); + const Accounts_Password_Policy_AtLeastOneSpecialCharacter = useSetting('Accounts_Password_Policy_AtLeastOneSpecialCharacter'); + const Accounts_Password_Policy_AtLeastOneUppercase = useSetting('Accounts_Password_Policy_AtLeastOneUppercase'); + const Accounts_Password_Policy_ForbidRepeatingCharacters = useSetting('Accounts_Password_Policy_ForbidRepeatingCharacters'); + const Accounts_Password_Policy_ForbidRepeatingCharactersCount = useSetting( + 'Accounts_Password_Policy_ForbidRepeatingCharactersCount' + ); + const Accounts_Password_Policy_MaxLength = useSetting('Accounts_Password_Policy_MaxLength'); + const Accounts_Password_Policy_MinLength = useSetting('Accounts_Password_Policy_MinLength'); + + const passwordPolicies: IPasswordPolicy[] | null = useMemo(() => { + if (!Accounts_Password_Policy_Enabled) return null; + + const policies = []; + + if (Accounts_Password_Policy_AtLeastOneLowercase) { + policies.push({ + name: 'AtLeastOneLowercase', + label: i18n.t('At_Least_1_Lowercase_Letter'), + regex: new RegExp('[a-z]') + }); + } + + if (Accounts_Password_Policy_AtLeastOneUppercase) { + policies.push({ + name: 'AtLeastOneUppercase', + label: i18n.t('At_Least_1_Uppercase_Letter'), + regex: new RegExp('[A-Z]') + }); + } + + if (Accounts_Password_Policy_AtLeastOneNumber) { + policies.push({ + name: 'AtLeastOneNumber', + label: i18n.t('At_Least_1_Number'), + regex: new RegExp('[0-9]') + }); + } + + if (Accounts_Password_Policy_AtLeastOneSpecialCharacter) { + policies.push({ + name: 'AtLeastOneSpecialCharacter', + label: i18n.t('At_Least_1_Symbol'), + regex: new RegExp('[^A-Za-z0-9 ]') + }); + } + + if (Accounts_Password_Policy_ForbidRepeatingCharacters) { + policies.push({ + name: 'ForbidRepeatingCharacters', + label: i18n.t('Max_Repeating_Characters', { quantity: Accounts_Password_Policy_ForbidRepeatingCharactersCount }), + regex: new RegExp(`(.)\\1{${Accounts_Password_Policy_ForbidRepeatingCharactersCount},}`) + }); + } + + if (Accounts_Password_Policy_MaxLength !== -1) { + policies.push({ + name: 'MaxLength', + label: i18n.t('At_Most_Characters', { quantity: Accounts_Password_Policy_MaxLength }), + regex: new RegExp(`.{1,${Accounts_Password_Policy_MaxLength}}`) + }); + } + + if (Accounts_Password_Policy_MinLength !== -1) { + policies.push({ + name: 'MinLength', + label: i18n.t('At_Least_Characters', { quantity: Accounts_Password_Policy_MinLength }), + regex: new RegExp(`.{${Accounts_Password_Policy_MinLength},}`) + }); + } + + return policies; + }, [ + Accounts_Password_Policy_AtLeastOneLowercase, + Accounts_Password_Policy_Enabled, + Accounts_Password_Policy_AtLeastOneNumber, + Accounts_Password_Policy_AtLeastOneSpecialCharacter, + Accounts_Password_Policy_AtLeastOneUppercase, + Accounts_Password_Policy_ForbidRepeatingCharacters, + Accounts_Password_Policy_ForbidRepeatingCharactersCount, + Accounts_Password_Policy_MaxLength, + Accounts_Password_Policy_MinLength + ]); + + const isPasswordValid = () => { + if (password !== confirmPassword) return false; + + if (!passwordPolicies) return true; + return passwordPolicies.every(policy => policy.regex.test(password)); + }; + + return { + passwordPolicies, + isPasswordValid + }; +}; + +export default useVerifyPassword; diff --git a/app/views/RegisterView/PasswordTips.tsx b/app/views/RegisterView/PasswordTips.tsx index 6d41091c49..0d3252055f 100644 --- a/app/views/RegisterView/PasswordTips.tsx +++ b/app/views/RegisterView/PasswordTips.tsx @@ -1,13 +1,14 @@ import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; +import { IPasswordPolicy } from '../../lib/hooks/useVerifyPassword'; import Tip from './Tip'; import i18n from '../../i18n'; import { useTheme } from '../../theme'; import sharedStyles from '../Styles'; const styles = StyleSheet.create({ - PasswordTipsTitle: { + passwordTipsTitle: { ...sharedStyles.textMedium, fontSize: 14, lineHeight: 20 @@ -21,21 +22,22 @@ const styles = StyleSheet.create({ interface IPasswordTips { isDirty: boolean; password: string; + tips: IPasswordPolicy[]; } -const PasswordTips = ({ isDirty, password }: IPasswordTips) => { +const PasswordTips = ({ isDirty, password, tips }: IPasswordTips) => { const { colors } = useTheme(); - const atLeastEightCharactersValidation = /^.{8,}$/; - const atMostTwentyFourCharactersValidation = /^.{0,24}$/; - const maxTwoRepeatingCharacters = /^(?!.*(.)\1\1)/; - const atLeastOneLowercaseLetter = /[a-z]/; - const atLeastOneNumber = /\d/; - const atLeastOneSymbol = /[^a-zA-Z0-9]/; - - const selectTipIconType = (validation: RegExp) => { + const selectTipIconType = (name: string, validation: RegExp) => { if (!isDirty) return 'info'; + // This regex checks if there are more than 3 consecutive repeating characters in a string. + // If the test is successful, the error icon and color should be selected. + if (name === 'ForbidRepeatingCharacters') { + if (!validation.test(password)) return 'success'; + return 'error'; + } + if (validation.test(password)) return 'success'; return 'error'; @@ -46,16 +48,13 @@ const PasswordTips = ({ isDirty, password }: IPasswordTips) => { + style={[styles.passwordTipsTitle, { color: colors.fontDefault }]}> {i18n.t('Your_Password_Must_Have')} - - - - - - + {tips.map(item => ( + + ))} ); diff --git a/app/views/RegisterView/index.tsx b/app/views/RegisterView/index.tsx index 81a157d14e..dccb84d466 100644 --- a/app/views/RegisterView/index.tsx +++ b/app/views/RegisterView/index.tsx @@ -1,6 +1,5 @@ import React, { useLayoutEffect, useState } from 'react'; import { Keyboard, Text, View } from 'react-native'; -import RNPickerSelect from 'react-native-picker-select'; import parse from 'url-parse'; import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; @@ -24,20 +23,16 @@ import { Services } from '../../lib/services'; import UGCRules from '../../containers/UserGeneratedContentRules'; import { useAppSelector } from '../../lib/hooks'; import PasswordTips from './PasswordTips'; -import getParsedCustomFields from './methods/getParsedCustomFields'; import getCustomFields from './methods/getCustomFields'; +import useVerifyPassword from '../../lib/hooks/useVerifyPassword'; +import CustomFields from '../../containers/CustomFields'; +import useParsedCustomFields from '../../lib/hooks/useCustomFields'; import styles from './styles'; -const passwordRules = /^(?!.*(.)\1{2})^(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,24}$/; const validationSchema = yup.object().shape({ name: yup.string().min(1).required(), email: yup.string().email().required(), - username: yup.string().min(1).required(), - password: yup.string().matches(passwordRules).required(), - confirmPassword: yup - .string() - .oneOf([yup.ref('password'), null]) - .required() + username: yup.string().min(1).required() }); interface IProps extends IBaseScreen {} @@ -71,10 +66,13 @@ const RegisterView = ({ navigation, route }: IProps) => { resolver: yupResolver(validationSchema) }); const password = watch('password'); - const parsedCustomFields = getParsedCustomFields(Accounts_CustomFields); + const confirmPassword = watch('confirmPassword'); + const { parsedCustomFields } = useParsedCustomFields(Accounts_CustomFields); const [customFields, setCustomFields] = useState(getCustomFields(parsedCustomFields)); const [saving, setSaving] = useState(false); + const { passwordPolicies, isPasswordValid } = useVerifyPassword(password, confirmPassword); + const login = () => { navigation.navigate('LoginView', { title: new parse(Site_Url).hostname }); }; @@ -134,71 +132,6 @@ const RegisterView = ({ navigation, route }: IProps) => { } }; - const renderCustomFields = () => { - if (!Accounts_CustomFields) { - return null; - } - try { - return ( - <> - {Object.keys(parsedCustomFields).map((key, index, array) => { - if (parsedCustomFields[key].type === 'select') { - const options = parsedCustomFields[key].options.map((option: string) => ({ label: option, value: option })); - return ( - { - const newValue: { [key: string]: string | number } = {}; - newValue[key] = value; - setCustomFields({ customFields: { ...customFields, ...newValue } }); - }} - value={customFields[key]}> - { - // @ts-ignore - this[key] = e; - }} - placeholder={key} - value={customFields[key]} - testID='register-view-custom-picker' - /> - - ); - } - - return ( - { - // @ts-ignore - this[key] = e; - }} - key={key} - label={key} - placeholder={key} - value={customFields[key]} - onChangeText={(value: string) => { - const newValue: { [key: string]: string | number } = {}; - newValue[key] = value; - setCustomFields({ customFields: { ...customFields, ...newValue } }); - }} - onSubmitEditing={() => { - if (array.length - 1 > index) { - // @ts-ignore - return this[array[index + 1]].focus(); - } - }} - containerStyle={styles.inputContainer} - /> - ); - })} - - ); - } catch (error) { - return null; - } - }; - useLayoutEffect(() => { navigation.setOptions({ title: route?.params?.title, @@ -320,11 +253,16 @@ const RegisterView = ({ navigation, route }: IProps) => { /> )} /> - {renderCustomFields()} + setCustomFields(value)} + /> - + {passwordPolicies && } +