From 8f60d6f7025a3f5c3363713ed96f32bd859a28e2 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak <68298935+m4tewoosh@users.noreply.github.com> Date: Fri, 3 Jan 2025 16:41:02 +0100 Subject: [PATCH] feat/MSSDK-2032: Fix the code quality issues with HIGH severity in mediastore-sdk (#441) * feat/MSSDK-2032: remove planDetails.js * feat/MSSDK-2032: refactor CaptureForm useEffect * feat/MSSDK-2032: remove await before onSubmit * feat/MSSDK-2032: extract shouldHideValue to function * feat/MSSDK-2032: remove duplicated type * feat/MSSDK-2032: rewrite PasswordInput to TypeScript, use Input instead of LegacyInput, refactor changing password strength * feat/MSSDK-2032: add getPrice function * feat/MSSDK-2032: add regex comments, add lookahead to eliminate backtracking * feat/MSSDK-2032: rewrite UpdateProfile to functional and TypeScript * feat/MSSDK-2032: fix regexes vulnerability to super-linear runtime due to backtracking * feat/MSSDK-2032: change two special characters regex * feat/MSSDK-2032: change or to nullish coalescence * feat/MSSDK-2032: change setter name * changes from CR * feat/MSSDK-2032: fix typescript issues * feat/MSSDK-2032: bring back UpdateProfile.container --------- Co-authored-by: Aleksander Sadaj --- src/appRedux/planDetails.js | 82 ----- src/appRedux/rootReducer.ts | 1 - .../AdditionalProfileInfo.tsx | 1 + .../AdditionalProfileInfo.types.ts | 4 +- .../AddressDetails/AddressDetails.types.ts | 20 +- src/components/Capture/Capture.tsx | 32 +- .../Capture/CaptureForm/CaptureForm.tsx | 78 +++-- src/components/CouponInput/CouponInput.tsx | 2 +- .../RecipientForm/RecipientForm.tsx | 35 +- .../InnerPopupWrapperStyled.ts | 1 - src/components/Input/Input.tsx | 3 + .../PasswordInput/PasswordInput.jsx | 161 ---------- .../PasswordInput/PasswordInput.tsx | 53 ++++ .../PasswordInput/{index.js => index.ts} | 0 .../PasswordInput/usePasswordInput.ts | 54 ++++ src/components/RegisterPage/RegisterForm.tsx | 1 - .../SwitchPlanPopup/SwitchPlanPopup.jsx | 53 +--- .../UpdateProfile/UpdateProfile.tsx | 231 ++++++++++++++ .../UpdateProfile/UpdateProfile.types.ts | 56 ++++ .../UpdateProfile/UpdateProfileStyled.ts} | 8 +- src/components/UpdateProfile/constants.ts | 7 + src/components/UpdateProfile/index.ts | 3 + src/components/UpdateProfile/utils.ts | 23 ++ .../UpdateProfile/UpdateProfile.component.jsx | 299 ------------------ ...ntainer.jsx => UpdateProfile.container.js} | 3 +- .../Capture => types}/Capture.types.ts | 26 +- src/util/capture.ts | 6 + src/util/generic.ts | 4 + src/util/passwordHelper.ts | 38 +++ 29 files changed, 605 insertions(+), 680 deletions(-) delete mode 100644 src/appRedux/planDetails.js delete mode 100644 src/components/PasswordInput/PasswordInput.jsx create mode 100644 src/components/PasswordInput/PasswordInput.tsx rename src/components/PasswordInput/{index.js => index.ts} (100%) create mode 100644 src/components/PasswordInput/usePasswordInput.ts create mode 100644 src/components/UpdateProfile/UpdateProfile.tsx create mode 100644 src/components/UpdateProfile/UpdateProfile.types.ts rename src/{containers/UpdateProfile/UpdateProfileStyled.js => components/UpdateProfile/UpdateProfileStyled.ts} (66%) create mode 100644 src/components/UpdateProfile/constants.ts create mode 100644 src/components/UpdateProfile/index.ts create mode 100644 src/components/UpdateProfile/utils.ts delete mode 100644 src/containers/UpdateProfile/UpdateProfile.component.jsx rename src/containers/UpdateProfile/{UpdateProfile.container.jsx => UpdateProfile.container.js} (95%) rename src/{components/Capture => types}/Capture.types.ts (62%) create mode 100644 src/util/capture.ts create mode 100644 src/util/generic.ts create mode 100644 src/util/passwordHelper.ts diff --git a/src/appRedux/planDetails.js b/src/appRedux/planDetails.js deleted file mode 100644 index 2dab5be40..000000000 --- a/src/appRedux/planDetails.js +++ /dev/null @@ -1,82 +0,0 @@ -// WHOLE FILE TO BE DELETED? - let me know in code review if this planDetails.js is needed - -/* eslint-disable no-param-reassign */ -import { createAction, createReducer } from '@reduxjs/toolkit'; - -export const SET_CURRENT_PLAN = 'SET_CURRENT_PLAN'; -export const setCurrentPlan = createAction(SET_CURRENT_PLAN); - -export const UPDATE_LIST = 'UPDATE_LIST'; -export const updateList = createAction(UPDATE_LIST); - -export const SET_OFFER_TO_SWITCH = 'SET_OFFER_TO_SWITCH'; -export const setOfferToSwitch = createAction(SET_OFFER_TO_SWITCH); - -export const SET_SWITCH_SETTINGS = 'SET_SWITCH_SETTINGS'; -export const setSwitchSettings = createAction(SET_SWITCH_SETTINGS); - -export const SET_SWITCH_DETAILS = 'SET_SWITCH_DETAILS'; -export const setSwitchDetails = createAction(SET_SWITCH_DETAILS); - -export const POPULATE_SWITCH_TITLE = 'POPULATE_SWITCH_TITLE'; -export const populateSwitchTitle = createAction(POPULATE_SWITCH_TITLE); - -const initialState = { - currentPlan: [], - updateList: false, - offerToSwitch: {}, - switchSettings: {}, - switchDetails: {} -}; - -const paymentDetailsReducer = createReducer(initialState, (builder) => { - builder.addCase(SET_CURRENT_PLAN, (state, action) => { - state.currentPlan = action.payload; - }); - builder.addCase(UPDATE_LIST, (state) => { - state.updateList = !state.updateList; - }); - builder.addCase(SET_OFFER_TO_SWITCH, (state, action) => { - state.offerToSwitch = action.payload; - }); - builder.addCase(SET_SWITCH_DETAILS, (state, action) => { - const { details, type } = action.payload; - if (type === 'delete') { - delete state.switchDetails[details.pendingSwitchId]; - } else { - state.switchDetails = Object.assign(state.switchDetails, details); - } - }); - builder.addCase(SET_SWITCH_SETTINGS, (state, action) => { - state.switchSettings[action.payload.offerId] = action.payload.settings; - }); - builder.addCase(POPULATE_SWITCH_TITLE, (state) => { - const switchesToFulfill = []; - Object.keys(state.switchDetails).forEach((pendingSwitchId) => { - if (!state.switchDetails[pendingSwitchId].title) { - switchesToFulfill.push(pendingSwitchId); - } - }); - if (switchesToFulfill.length && state.switchSettings) { - switchesToFulfill.forEach((pendingSwitchId) => { - Object.keys(state.switchSettings).forEach((offerId) => { - const switchSettingsDetails = state.switchSettings[ - offerId - ].available.find( - (item) => - item.toOfferId === state.switchDetails[pendingSwitchId].toOfferId - ); - if (switchSettingsDetails) { - const { title } = switchSettingsDetails; - state.switchDetails[pendingSwitchId] = { - ...state.switchDetails[pendingSwitchId], - title - }; - } - }); - }); - } - }); -}); - -export default paymentDetailsReducer; diff --git a/src/appRedux/rootReducer.ts b/src/appRedux/rootReducer.ts index a1e47e3da..0b6b5e7a1 100644 --- a/src/appRedux/rootReducer.ts +++ b/src/appRedux/rootReducer.ts @@ -41,7 +41,6 @@ const rootReducer = combineReducers({ paymentDetails: paymentDetailsReducer, paymentMethods: paymentMethodsReducer, plan: planDetailsReducer, - // removed planDetails - to be checked if was needed popup: popupReducer, popupManager: popupManagerReducer, publisherConfig: publisherConfigReducer, diff --git a/src/components/AdditionalProfileInfo/AdditionalProfileInfo.tsx b/src/components/AdditionalProfileInfo/AdditionalProfileInfo.tsx index 47ee85b7c..6eb355292 100644 --- a/src/components/AdditionalProfileInfo/AdditionalProfileInfo.tsx +++ b/src/components/AdditionalProfileInfo/AdditionalProfileInfo.tsx @@ -41,6 +41,7 @@ const AdditionalProfileInfo = ({ const newData: CustomSetting[] = data.map((setting) => { return { ...setting, + answer: setting.answer ?? '', value: setting.answer ? setting.answer : '', values: setting.value ? setting.value.split(';').map((v) => v.trim()) diff --git a/src/components/AdditionalProfileInfo/AdditionalProfileInfo.types.ts b/src/components/AdditionalProfileInfo/AdditionalProfileInfo.types.ts index 4bc5607ad..a96414586 100644 --- a/src/components/AdditionalProfileInfo/AdditionalProfileInfo.types.ts +++ b/src/components/AdditionalProfileInfo/AdditionalProfileInfo.types.ts @@ -1,3 +1,5 @@ +import { CustomCaptureSetting } from 'types/Capture.types'; + export type CustomSetting = { answer?: string; key: string; @@ -7,7 +9,7 @@ export type CustomSetting = { }; export type AdditionalProfileInfoProps = { - data: CustomSetting[] | null; + data: CustomCaptureSetting[] | null; isLoading: boolean; updateCaptureOption: (option: { key: string; value: string }) => void; }; diff --git a/src/components/AddressDetails/AddressDetails.types.ts b/src/components/AddressDetails/AddressDetails.types.ts index 615497138..45a2fdbf4 100644 --- a/src/components/AddressDetails/AddressDetails.types.ts +++ b/src/components/AddressDetails/AddressDetails.types.ts @@ -1,18 +1,16 @@ +import { AddressCaptureSetting } from 'types/Capture.types'; + export type Address = { - address: string | null; - address2: string | null; - city: string | null; - state: string | null; - postCode: string | null; + address: string; + address2: string; + city: string; + country: string; + state: string; + postCode: string; }; export type AddressDetailsProps = { - data: { - answer: Address; - enabled: boolean; - key: string; - required: boolean; - }; + data: AddressCaptureSetting; isLoading?: boolean; updateCaptureOption: (params: { key: string; value: Address }) => void; }; diff --git a/src/components/Capture/Capture.tsx b/src/components/Capture/Capture.tsx index 60f2ddd7f..545d7fe61 100644 --- a/src/components/Capture/Capture.tsx +++ b/src/components/Capture/Capture.tsx @@ -4,7 +4,7 @@ import Header from 'components/Header'; import Footer from 'components/Footer'; import Loader from 'components/Loader'; import getCaptureStatus from 'api/Customer/getCaptureStatus'; -import { CaptureProps, CaptureSetting } from './Capture.types'; +import { CaptureProps, CaptureSetting } from '../../types/Capture.types'; import CaptureForm from './CaptureForm/CaptureForm'; import { @@ -15,25 +15,22 @@ import { const noop = () => null; -const Capture = ({ settings = [], onSuccess = noop }: CaptureProps) => { +const Capture = ({ onSuccess = noop }: CaptureProps) => { const { t } = useTranslation(); const [captureSettings, setCaptureSettings] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { - if (settings.length) { - setCaptureSettings(settings); - setIsLoading(false); - } else { - getCaptureStatus().then((resp) => { - if (resp.responseData.shouldCaptureBeDisplayed === true) { - setCaptureSettings(resp.responseData.settings); - setIsLoading(false); - } else { - onSuccess(); - } - }); - } + getCaptureStatus().then(({ responseData }) => { + const { shouldCaptureBeDisplayed, settings } = responseData; + + if (shouldCaptureBeDisplayed) { + setCaptureSettings(settings); + setIsLoading(false); + } else { + onSuccess(); + } + }); }, []); return ( @@ -47,10 +44,7 @@ const Capture = ({ settings = [], onSuccess = noop }: CaptureProps) => { {t('capture.confirm-registration', 'Confirm Registration')} - + )} diff --git a/src/components/Capture/CaptureForm/CaptureForm.tsx b/src/components/Capture/CaptureForm/CaptureForm.tsx index 120bb18b1..7ca57be27 100644 --- a/src/components/Capture/CaptureForm/CaptureForm.tsx +++ b/src/components/Capture/CaptureForm/CaptureForm.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { updateCaptureAnswers } from 'api'; import { validateEmailField } from 'util/validators'; import { PHONE_NUMBER_REGEX } from 'util/regexConstants'; +import { isCustomSetting } from 'util/capture'; import Input from 'components/Input'; import EmailInput from 'components/EmailInput'; import DateInput from 'components/DateInput'; @@ -12,10 +13,9 @@ import Button from 'components/Button'; import Loader from 'components/Loader'; import useInput from './useInput'; import { - CaptureProps, - CaptureSetting, + CaptureFormProps, CustomCaptureSetting -} from '../Capture.types'; +} from '../../../types/Capture.types'; import { CaptureRowStyled, CaptureBoxStyled, @@ -23,16 +23,12 @@ import { CaptureError } from './CaptureFormStyled'; -const isCustomSetting = ( - setting: CaptureSetting -): setting is CustomCaptureSetting => setting.key.startsWith('custom_'); - type CustomSetting = CustomCaptureSetting & { values: Array<{ value: string; label: string }>; error?: string | null; }; -const CaptureForm = ({ settings, onSuccess }: CaptureProps) => { +const CaptureForm = ({ settings, onSuccess }: CaptureFormProps) => { const { t } = useTranslation(); const [processing, setProcessing] = useState(false); const [customSettings, setCustomSettings] = useState([]); @@ -54,36 +50,54 @@ const CaptureForm = ({ settings, onSuccess }: CaptureProps) => { }; useEffect(() => { - for (let i = 0; i < settings.length; i += 1) { - const item = settings[i]; - if (item.key === 'firstNameLastName' && item.answer) { - firstName.setValue(item.answer.firstName); - lastName.setValue(item.answer.lastName); + settings.forEach((setting) => { + const { answer, key } = setting; + + if (!answer) { + return; } - if (item.key === 'birthDate' && item.answer) - birthDate.setValue(item.answer); - if (item.key === 'companyName' && item.answer) - companyName.setValue(item.answer); - if (item.key === 'phoneNumber' && item.answer) - phoneNumber.setValue(item.answer); - if (item.key === 'address' && item.answer) { - address.setValue(item.answer.address); - address2.setValue(item.answer.address2); - city.setValue(item.answer.city); - state.setValue(item.answer.state); - postCode.setValue(item.answer.postCode); + + switch (key) { + case 'firstNameLastName': + firstName.setValue(answer.firstName); + lastName.setValue(answer.lastName); + break; + + case 'birthDate': + birthDate.setValue(answer); + break; + + case 'companyName': + companyName.setValue(answer); + break; + + case 'phoneNumber': + phoneNumber.setValue(answer); + break; + + case 'address': + address.setValue(answer.address); + address2.setValue(answer.address2); + city.setValue(answer.city); + state.setValue(answer.state); + postCode.setValue(answer.postCode); + break; + + default: + break; } - } + }); const enabledCustomSettings: CustomSetting[] = settings.filter( - (item) => isCustomSetting(item) && item.enabled + (setting) => isCustomSetting(setting) && setting.enabled ) as CustomSetting[]; + const transformedSettings: CustomSetting[] = enabledCustomSettings.map( - (item) => ({ - ...item, - value: item.answer ? item.answer : '', - values: isCustomSetting(item) - ? item.value.split(';').map((i) => { + (setting) => ({ + ...setting, + value: setting.answer ? setting.answer : '', + values: isCustomSetting(setting) + ? setting.value.split(';').map((i) => { const value = i.trim(); const label = value; return { diff --git a/src/components/CouponInput/CouponInput.tsx b/src/components/CouponInput/CouponInput.tsx index 2316b16dc..4003e40e0 100644 --- a/src/components/CouponInput/CouponInput.tsx +++ b/src/components/CouponInput/CouponInput.tsx @@ -46,7 +46,7 @@ const CouponInput = ({ setIsOpen(true); } else { eventDispatcher(MSSDK_REDEEM_BUTTON_CLICKED, { coupon: value, source }); - await onSubmit(value); + onSubmit(value); } }; diff --git a/src/components/DeliveryDetails/RecipientForm/RecipientForm.tsx b/src/components/DeliveryDetails/RecipientForm/RecipientForm.tsx index 8bf6cb3fa..4d1b00359 100644 --- a/src/components/DeliveryDetails/RecipientForm/RecipientForm.tsx +++ b/src/components/DeliveryDetails/RecipientForm/RecipientForm.tsx @@ -50,7 +50,7 @@ const RecipientForm = ({ isMyAccount = false }: RecipientFormProps) => { const { t } = useTranslation(); - const onBlur = (e: React.FocusEvent) => { + const handleBlur = (e: React.FocusEvent) => { const { target: { name, value } } = e; @@ -77,7 +77,7 @@ const RecipientForm = ({ isMyAccount = false }: RecipientFormProps) => { } }; - const onChange = ( + const handleChange = ( e: React.ChangeEvent ) => { if (shouldHideValue) { @@ -134,6 +134,9 @@ const RecipientForm = ({ isMyAccount = false }: RecipientFormProps) => { const shouldHideValue = !isMyAccount && isEditDeliveryDetailsPopupOpened; + const getInputValue = (value: string | number) => + shouldHideValue ? '' : value; + return ( {loading ? ( @@ -158,14 +161,14 @@ const RecipientForm = ({ isMyAccount = false }: RecipientFormProps) => { error={t(recipientEmail.translationKey, recipientEmail.error)} label={t('recipientForm.label.recipient-email', 'Recipient email')} name='recipientEmail' - onBlur={onBlur} - onChange={onChange} + onBlur={handleBlur} + onChange={handleChange} placeholder={t( 'recipientForm.label.recipient-email', 'jdoe@cleeng.com' )} type='email' - value={shouldHideValue ? '' : recipientEmail.value} + value={getInputValue(recipientEmail.value)} /> {!isFieldDisabled && ( { 'Confirm recipient email' )} name='confirmRecipientEmail' - onBlur={onBlur} - onChange={onChange} + onBlur={handleBlur} + onChange={handleChange} placeholder={t( 'recipientForm.label.confirm-recipient-email', 'jdoe@cleeng.com' )} type='email' - value={shouldHideValue ? '' : confirmRecipientEmail.value} + value={getInputValue(confirmRecipientEmail.value)} /> )} @@ -195,10 +198,10 @@ const RecipientForm = ({ isMyAccount = false }: RecipientFormProps) => { label={t('recipientForm.label.delivery-date', 'Delivery date')} min={new Date().toISOString().split('T')[0]} name='deliveryDate' - onBlur={onBlur} - onChange={onChange} + onBlur={handleBlur} + onChange={handleChange} type='date' - value={shouldHideValue ? '' : deliveryDate.value} + value={getInputValue(deliveryDate.value)} width='50%' /> { error={t(deliveryTime.translationKey, deliveryTime.error)} label={t('recipientForm.label.delivery-time', 'Delivery time')} name='deliveryTime' - onBlur={onBlur} - onChange={onChange} + onBlur={handleBlur} + onChange={handleChange} type='time' - value={shouldHideValue ? '' : deliveryTime.value} + value={getInputValue(deliveryTime.value)} width='50%' /> @@ -225,7 +228,7 @@ const RecipientForm = ({ isMyAccount = false }: RecipientFormProps) => { disabled={isFieldDisabled} maxLength={150} name='message' - onChange={onChange} + onChange={handleChange} placeholder={ isFieldDisabled ? '' @@ -235,7 +238,7 @@ const RecipientForm = ({ isMyAccount = false }: RecipientFormProps) => { ) } rows={3} - value={shouldHideValue ? '' : message.value} + value={getInputValue(message.value)} /> {!isMyAccount && ( diff --git a/src/components/InnerPopupWrapper/InnerPopupWrapperStyled.ts b/src/components/InnerPopupWrapper/InnerPopupWrapperStyled.ts index e24ec682e..9d8ce3db2 100644 --- a/src/components/InnerPopupWrapper/InnerPopupWrapperStyled.ts +++ b/src/components/InnerPopupWrapper/InnerPopupWrapperStyled.ts @@ -108,7 +108,6 @@ type TitleStyledProps = { | 'inherit' | 'initial' | 'revert' - | 'revert' | 'revert-layer' | 'unset'; diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index d3e972f32..c6216155c 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -27,6 +27,7 @@ type InputProps = { showPassword?: boolean; passwordStrength?: PasswordStrength; required?: boolean; + ariaRequired?: boolean; invalid?: boolean; icon?: ReactNode; floatingLabels?: boolean; @@ -48,6 +49,7 @@ const Input = ({ invalid, icon, required, + ariaRequired, floatingLabels, reference, format @@ -67,6 +69,7 @@ const Input = ({ onBlur={onBlur} ref={reference} required={required} + aria-required={ariaRequired} aria-invalid={invalid} $withIcon={!!icon} $floatingLabels={!!floatingLabels} diff --git a/src/components/PasswordInput/PasswordInput.jsx b/src/components/PasswordInput/PasswordInput.jsx deleted file mode 100644 index f6a87619d..000000000 --- a/src/components/PasswordInput/PasswordInput.jsx +++ /dev/null @@ -1,161 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import InputLegacy from 'components/InputLegacy'; - -class PasswordInput extends React.Component { - constructor(props) { - super(props); - this.state = { - passError: '', - errorLabel: '' - }; - } - - onChangeFunction = (value) => { - const { onChange, showPasswordStrength } = this.props; - if (showPasswordStrength) { - const passwordStrength = this.validateNewPassword(value); - this.setState({ - passError: this.getErrorMessage(passwordStrength), - errorLabel: passwordStrength - }); - } - onChange(value); - }; - - validateNewPassword = (pass) => { - let score = 0; - if ( - pass && - pass.length >= 8 && - pass.match(/\d+/) && - pass.match(/[a-zA-Z]/) - ) { - if (pass.match(/[a-z]/)) { - score += 1; - } - if (pass.match(/[A-Z]/)) { - score += 5; - } - if (pass.match(/\d+/) && !pass.match(/^[0-9]*$/)) { - score += 5; - } - if (pass.match(/(\d.*\d)/)) { - score += 5; - } - if (pass.match(/[!,@#$%^&*?_~]/)) { - score += 5; - } - if (pass.match(/([!,@#$%^&*?_~].*[!,@#$%^&*?_~])/)) { - score += 5; - } - if (pass.match(/[a-z]/) && pass.match(/[A-Z]/)) { - score += 2; - } - if (pass.match(/\d/) && pass.match(/\D/)) { - score += 2; - } - if ( - pass.match(/[a-z]/) && - pass.match(/[A-Z]/) && - pass.match(/\d/) && - pass.match(/[!,@#$%^&*?_~]/) - ) { - score += 2; - } - if (score <= 8) { - return 'Weak'; - } - if (score > 8 && score <= 16) { - return 'Fair'; - } - if (score > 16 && score <= 24) { - return 'Good'; - } - if (score > 24 && score <= 32) { - return 'Strong'; - } - } - return 'NotValid'; - }; - - getErrorMessage = (msg) => { - const { t } = this.props; - const errorLabel = { - Weak: t('password-input.error.weak', 'Weak'), - Fair: t('password-input.error.fair', 'Could be stronger'), - Good: t('password-input.error.good', 'Good password'), - Strong: t('password-input.error.strong', 'Strong password'), - NotValid: t( - 'password-input.error.not-valid', - 'Your password must contain at least 8 characters, including 1 digit.' - ) - }; - - return errorLabel[msg]; - }; - - render() { - const { - value, - onBlur, - error, - showVisibilityIcon, - showPassword, - handleClickShowPassword, - label, - floatingLabels - } = this.props; - const { passError, errorLabel } = this.state; - const errorMsg = error || passError; - return ( - <> - - - ); - } -} - -PasswordInput.propTypes = { - value: PropTypes.string, - onChange: PropTypes.func, - onBlur: PropTypes.func, - error: PropTypes.string, - showVisibilityIcon: PropTypes.bool, - showPassword: PropTypes.bool, - handleClickShowPassword: PropTypes.func, - label: PropTypes.string, - floatingLabels: PropTypes.bool, - showPasswordStrength: PropTypes.bool, - t: PropTypes.func -}; - -PasswordInput.defaultProps = { - value: '', - onChange: () => null, - onBlur: () => null, - error: '', - showVisibilityIcon: false, - showPassword: false, - handleClickShowPassword: () => null, - label: 'Password', - floatingLabels: true, - showPasswordStrength: false, - t: (k) => k -}; - -export default PasswordInput; diff --git a/src/components/PasswordInput/PasswordInput.tsx b/src/components/PasswordInput/PasswordInput.tsx new file mode 100644 index 000000000..3d0501adc --- /dev/null +++ b/src/components/PasswordInput/PasswordInput.tsx @@ -0,0 +1,53 @@ +import Input from 'components/Input'; +import usePasswordInput from './usePasswordInput'; + +type PasswordInputProps = { + value: string; + onBlur: () => void; + error: string; + onChange: (value: string) => void; + showVisibilityIcon: boolean; + showPassword: boolean; + showPasswordStrength: boolean; + handleClickShowPassword: () => void; + label: string; + floatingLabels: boolean; +}; + +const PasswordInput = ({ + value, + onBlur, + error, + onChange, + showVisibilityIcon, + showPasswordStrength, + showPassword, + handleClickShowPassword, + label = 'Password', + floatingLabels = true +}: PasswordInputProps) => { + const { passwordError, strengthLabel, handleChange } = usePasswordInput({ + onChange, + showPasswordStrength + }); + + return ( + + ); +}; + +export default PasswordInput; diff --git a/src/components/PasswordInput/index.js b/src/components/PasswordInput/index.ts similarity index 100% rename from src/components/PasswordInput/index.js rename to src/components/PasswordInput/index.ts diff --git a/src/components/PasswordInput/usePasswordInput.ts b/src/components/PasswordInput/usePasswordInput.ts new file mode 100644 index 000000000..283d5b1b5 --- /dev/null +++ b/src/components/PasswordInput/usePasswordInput.ts @@ -0,0 +1,54 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PasswordStrength } from 'types/generic.types'; +import getPasswordStrength from 'util/passwordHelper'; + +type UsePasswordInputArgs = { + onChange: (value: string) => void; + showPasswordStrength: boolean; +}; + +const usePasswordInput = ({ + onChange, + showPasswordStrength +}: UsePasswordInputArgs) => { + const { t } = useTranslation(); + + const [passwordError, setPasswordError] = useState(''); + const [strengthLabel, setStrengthLabel] = useState(); + + const getErrorMessage = (message: PasswordStrength) => { + const errorMessageLabel = { + Weak: t('password-input.error.weak', 'Weak'), + Fair: t('password-input.error.fair', 'Could be stronger'), + Good: t('password-input.error.good', 'Good password'), + Strong: t('password-input.error.strong', 'Strong password'), + NotValid: t( + 'password-input.error.not-valid', + 'Your password must contain at least 8 characters, including 1 digit.' + ) + }; + + return errorMessageLabel[message]; + }; + + const handleChange = (password: string) => { + if (showPasswordStrength) { + const passwordStrength = getPasswordStrength(password); + + setPasswordError(getErrorMessage(passwordStrength)); + setStrengthLabel(passwordStrength); + } + + onChange(password); + }; + + return { + passwordError, + strengthLabel, + getErrorMessage, + handleChange + }; +}; + +export default usePasswordInput; diff --git a/src/components/RegisterPage/RegisterForm.tsx b/src/components/RegisterPage/RegisterForm.tsx index 972f8c2b6..a818a6aea 100644 --- a/src/components/RegisterPage/RegisterForm.tsx +++ b/src/components/RegisterPage/RegisterForm.tsx @@ -224,7 +224,6 @@ export const RegisterForm = ({ onSuccess }: RegisterFormProps) => { showPassword={showPassword} handleClickShowPassword={handleClickShowPassword} showPasswordStrength - t={t} />