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 && }
+