-
Notifications
You must be signed in to change notification settings - Fork 280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add password validation to ConfirmPassword
#2900
Changes from all commits
1290992
ea6d226
6e2a79d
d59b029
ca4f435
4425e61
15595b7
156c151
120e814
7a2254c
dad960d
fb424f9
d4952ab
ccf9234
245e88b
c4d9f96
097029e
ad222c4
33f88b5
15d2334
ab093a2
185c998
ffcad39
2a2767c
71c813d
ead7fae
7af71ba
2b67649
26d658a
762033e
1d90805
f94dd6d
8dd2b1b
a37d48a
9200fef
ec0261a
9bcc6f3
e8163d0
2b45df5
070a2c6
681f32e
e38dd89
6078356
4e570a5
8cb72b4
4277e36
d2da024
4ea77b6
ddfc4d8
5df789d
62bcf8a
0563f97
7cbfdf4
36405f0
05ae9b5
3c86e81
4e6fec3
6bc2edb
0e3911e
b6d5113
5d7f4a3
52d5b02
11593c3
c0403a1
dc6411f
7f84d0d
5058f22
a2a7f15
1388d25
77710d8
6b777b0
ff5dc10
aa7223a
627644d
c07c230
ee476ae
e187a92
9a9fa07
5c3906a
d560eaa
9a71373
78e73c2
7ff2acb
2a1d469
6a7349b
39a189c
b38848d
9771d2d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,149 @@ | ||
import React from 'react'; | ||
import isEqual from 'lodash/isEqual'; | ||
|
||
import { Logger } from 'aws-amplify'; | ||
import { changePassword, translate } from '@aws-amplify/ui'; | ||
import { | ||
changePassword, | ||
ValidatorOptions, | ||
getDefaultConfirmPasswordValidators, | ||
getDefaultPasswordValidators, | ||
runFieldValidators, | ||
translate, | ||
} from '@aws-amplify/ui'; | ||
|
||
import { useAuth } from '../../../internal'; | ||
import { View, Flex } from '../../../primitives'; | ||
import { FormValues, BlurredFields, ValidationError } from '../types'; | ||
import { | ||
DefaultCurrentPassword, | ||
DefaultPasswordField, | ||
DefaultError, | ||
DefaultNewPassword, | ||
DefaultSubmitButton, | ||
} from './defaultComponents'; | ||
import { ChangePasswordProps } from './types'; | ||
import { FormValues } from '../types'; | ||
import { ChangePasswordProps, ValidateParams } from './types'; | ||
|
||
const logger = new Logger('ChangePassword'); | ||
|
||
const getIsDisabled = ( | ||
formValues: FormValues, | ||
validationError: ValidationError | ||
): boolean => { | ||
const { currentPassword, newPassword, confirmPassword } = formValues; | ||
|
||
const hasEmptyField = !currentPassword || !newPassword || !confirmPassword; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't super important or anything but for readability it would be a bit easier if we just early return from this line since we know we don't need to evaluate the lengths of the validation errors below There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if (hasEmptyField) { | ||
return true; | ||
} | ||
|
||
const arePasswordsInvalid = | ||
validationError.newPassword?.length > 0 || | ||
validationError.confirmPassword?.length > 0; | ||
|
||
return arePasswordsInvalid; | ||
}; | ||
|
||
function ChangePassword({ | ||
onSuccess, | ||
onError, | ||
validators, | ||
}: ChangePasswordProps): JSX.Element | null { | ||
const [errorMessage, setErrorMessage] = React.useState<string>(null); | ||
const [formValues, setFormValues] = React.useState<FormValues>({}); | ||
|
||
const [validationError, setValidationError] = React.useState<ValidationError>( | ||
{} | ||
); | ||
const blurredFields = React.useRef<BlurredFields>([]); | ||
const { user, isLoading } = useAuth(); | ||
|
||
/** Return null if Auth.getCurrentAuthenticatedUser is still in progress */ | ||
if (isLoading) { | ||
return null; | ||
} | ||
const isDisabled = getIsDisabled(formValues, validationError); | ||
|
||
/** Return null if user isn't authenticated in the first place */ | ||
if (!user) { | ||
logger.warn('<ChangePassword /> requires user to be authenticated.'); | ||
return null; | ||
} | ||
const passwordValidators: ValidatorOptions[] = React.useMemo(() => { | ||
return validators ?? getDefaultPasswordValidators(); | ||
}, [validators]); | ||
|
||
/* | ||
* Note that formValues and other states are passed in as props so that | ||
* it does not depend on whether or not those states have been updated yet | ||
*/ | ||
const validateNewPassword = React.useCallback( | ||
({ formValues, eventType }: ValidateParams): string[] => { | ||
const { newPassword } = formValues; | ||
const hasBlurred = blurredFields.current.includes('newPassword'); | ||
|
||
return runFieldValidators({ | ||
value: newPassword, | ||
validators: passwordValidators, | ||
eventType, | ||
hasBlurred, | ||
}); | ||
}, | ||
[passwordValidators] | ||
); | ||
|
||
const validateConfirmPassword = React.useCallback( | ||
({ formValues, eventType }: ValidateParams): string[] => { | ||
const { newPassword, confirmPassword } = formValues; | ||
const hasBlurred = blurredFields.current.includes('confirmPassword'); | ||
|
||
const confirmPasswordValidators = | ||
getDefaultConfirmPasswordValidators(newPassword); | ||
|
||
return runFieldValidators({ | ||
value: confirmPassword, | ||
validators: confirmPasswordValidators, | ||
eventType, | ||
hasBlurred, | ||
}); | ||
}, | ||
[] | ||
); | ||
|
||
const runValidation = React.useCallback( | ||
(param: ValidateParams) => { | ||
const passwordErrors = validateNewPassword(param); | ||
const confirmPasswordErrors = validateConfirmPassword(param); | ||
|
||
/** Translations */ | ||
const newValidationError = { | ||
newPassword: passwordErrors, | ||
confirmPassword: confirmPasswordErrors, | ||
}; | ||
|
||
// only re-render if errors have changed | ||
if (!isEqual(validationError, newValidationError)) { | ||
setValidationError(newValidationError); | ||
} | ||
}, | ||
[validateConfirmPassword, validateNewPassword, validationError] | ||
); | ||
|
||
/* Translations */ | ||
// TODO: add AccountSettingsTextUtil to collect these strings | ||
const currentPasswordLabel = translate('Current Password'); | ||
const newPasswordLabel = translate('New Password'); | ||
const confirmPasswordLabel = translate('Confirm Password'); | ||
const updatePasswordText = translate('Update password'); | ||
|
||
/** Event Handlers */ | ||
/* Event Handlers */ | ||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
event.preventDefault(); | ||
|
||
const { name, value } = event.target; | ||
setFormValues({ ...formValues, [name]: value }); | ||
|
||
const newFormValues = { ...formValues, [name]: value }; | ||
runValidation({ formValues: newFormValues, eventType: 'change' }); | ||
|
||
setFormValues(newFormValues); | ||
}; | ||
|
||
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => { | ||
event.preventDefault(); | ||
|
||
const { name } = event.target; | ||
// only update state and run validation if this is the first time blurring the field | ||
if (!blurredFields.current.includes(name)) { | ||
const newBlurredFields = [...blurredFields.current, name]; | ||
blurredFields.current = newBlurredFields; | ||
runValidation({ formValues, eventType: 'blur' }); | ||
} | ||
}; | ||
|
||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { | ||
|
@@ -68,24 +164,47 @@ function ChangePassword({ | |
} | ||
}; | ||
|
||
// Return null if Auth.getCurrentAuthenticatedUser is still in progress | ||
if (isLoading) { | ||
return null; | ||
} | ||
|
||
// Return null if user isn't authenticated in the first place | ||
if (!user) { | ||
logger.warn('<ChangePassword /> requires user to be authenticated.'); | ||
return null; | ||
} | ||
|
||
return ( | ||
<View as="form" className="amplify-changepassword" onSubmit={handleSubmit}> | ||
<Flex direction="column"> | ||
<DefaultCurrentPassword | ||
<DefaultPasswordField | ||
autoComplete="current-password" | ||
isRequired | ||
label={currentPasswordLabel} | ||
name="currentPassword" | ||
onBlur={handleBlur} | ||
onChange={handleChange} | ||
/> | ||
<DefaultNewPassword | ||
<DefaultPasswordField | ||
autoComplete="new-password" | ||
isRequired | ||
label={newPasswordLabel} | ||
name="newPassword" | ||
onBlur={handleBlur} | ||
onChange={handleChange} | ||
validationErrors={validationError?.newPassword} | ||
/> | ||
<DefaultPasswordField | ||
autoComplete="new-password" | ||
isRequired | ||
label={confirmPasswordLabel} | ||
name="confirmPassword" | ||
onBlur={handleBlur} | ||
onChange={handleChange} | ||
validationErrors={validationError?.confirmPassword} | ||
/> | ||
<DefaultSubmitButton type="submit"> | ||
<DefaultSubmitButton isDisabled={isDisabled} type="submit"> | ||
{updatePasswordText} | ||
</DefaultSubmitButton> | ||
{errorMessage ? <DefaultError>{errorMessage}</DefaultError> : null} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pulled this outside
ChangePassword
because it's really an util that doesn't need to be redefined everytime.