Skip to content

Commit

Permalink
chore: created CustomFields component, useVerifyPassword and useCusto…
Browse files Browse the repository at this point in the history
…mFields
  • Loading branch information
OtavioStasiak committed Dec 11, 2024
1 parent 48ba260 commit 05cc7c1
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 110 deletions.
76 changes: 76 additions & 0 deletions app/containers/CustomFields/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<RNPickerSelect
key={key}
items={options}
onValueChange={value => {
const newValue: { [key: string]: string } = {};
newValue[key] = value;
onCustomFieldChange({ ...customFields, ...newValue });
}}
value={customFields[key]}>
<FormTextInput
inputRef={e => {
// @ts-ignore
this[key] = e;
}}
label={key}
placeholder={key}
value={customFields[key]}
testID='settings-view-language'
/>
</RNPickerSelect>
);
}

return (
<FormTextInput
inputRef={e => {
// @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;
27 changes: 27 additions & 0 deletions app/lib/constants/defaultSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
Expand Down
20 changes: 20 additions & 0 deletions app/lib/hooks/useCustomFields.ts
Original file line number Diff line number Diff line change
@@ -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;
112 changes: 112 additions & 0 deletions app/lib/hooks/useVerifyPassword.ts
Original file line number Diff line number Diff line change
@@ -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;
33 changes: 16 additions & 17 deletions app/views/RegisterView/PasswordTips.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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';
Expand All @@ -46,16 +48,13 @@ const PasswordTips = ({ isDirty, password }: IPasswordTips) => {
<Text
accessibilityLabel={i18n.t('Your_Password_Must_Have')}
accessible
style={[styles.PasswordTipsTitle, { color: colors.fontDefault }]}>
style={[styles.passwordTipsTitle, { color: colors.fontDefault }]}>
{i18n.t('Your_Password_Must_Have')}
</Text>
<View style={styles.tips}>
<Tip iconType={selectTipIconType(atLeastEightCharactersValidation)} description={i18n.t('At_Least_8_Characters')} />
<Tip iconType={selectTipIconType(atMostTwentyFourCharactersValidation)} description={i18n.t('At_Most_24_Characters')} />
<Tip iconType={selectTipIconType(maxTwoRepeatingCharacters)} description={i18n.t('Max_2_Repeating_Characters')} />
<Tip iconType={selectTipIconType(atLeastOneLowercaseLetter)} description={i18n.t('At_Least_1_Lowercase_Letter')} />
<Tip iconType={selectTipIconType(atLeastOneNumber)} description={i18n.t('At_Least_1_Number')} />
<Tip iconType={selectTipIconType(atLeastOneSymbol)} description={i18n.t('At_Least_1_Symbol')} />
{tips.map(item => (
<Tip iconType={selectTipIconType(item.name, item.regex)} description={item.label} />
))}
</View>
</View>
);
Expand Down
Loading

0 comments on commit 05cc7c1

Please sign in to comment.