import classnames from 'classnames'; import every from 'lodash/every'; import { ChangeEvent, useRef, useState } from 'react'; import { FaPencilAlt } from 'react-icons/fa'; import API from '../../../../../api'; import { ErrorKind, RefInputField } from '../../../../../types'; import alertDispatcher from '../../../../../utils/alertDispatcher'; import compoundErrorMessage from '../../../../../utils/compoundErrorMessage'; import InputField from '../../../../common/InputField'; interface Password { value: string; isValid: boolean; } interface FormValidation { isValid: boolean; oldPassword: string | null; newPassword: string | null; } const UpdatePassword = () => { const form = useRef<HTMLFormElement>(null); const oldPasswordInput = useRef<RefInputField>(null); const passwordInput = useRef<RefInputField>(null); const repeatPasswordInput = useRef<RefInputField>(null); const [isSending, setIsSending] = useState(false); const [password, setPassword] = useState<Password>({ value: '', isValid: false }); const [isValidated, setIsValidated] = useState(false); const onPasswordChange = (e: ChangeEvent<HTMLInputElement>) => { setPassword({ value: e.target.value, isValid: e.currentTarget.checkValidity() }); }; async function updatePassword(oldPassword: string, newPassword: string) { try { setIsSending(true); await API.updatePassword(oldPassword, newPassword); cleanForm(); setIsSending(false); setIsValidated(false); } catch (err: any) { setIsSending(false); if (err.kind !== ErrorKind.Unauthorized) { let error = compoundErrorMessage(err, 'An error occurred updating your password'); alertDispatcher.postAlert({ type: 'danger', message: error, }); } else { alertDispatcher.postAlert({ type: 'danger', message: 'An error occurred updating your password, please make sure you have entered your old password correctly', }); } } } const cleanForm = () => { oldPasswordInput.current!.reset(); passwordInput.current!.reset(); repeatPasswordInput.current!.reset(); }; const submitForm = () => { setIsSending(true); if (form.current) { validateForm(form.current).then((validation: FormValidation) => { if (validation.isValid) { updatePassword(validation.oldPassword!, validation.newPassword!); } else { setIsSending(false); } }); } }; const validateForm = async (form: HTMLFormElement): Promise<FormValidation> => { let newPassword: string | null = null; let oldPassword: string | null = null; return validateAllFields().then((isValid: boolean) => { if (isValid) { const formData = new FormData(form); newPassword = formData.get('password') as string; oldPassword = formData.get('oldPassword') as string; } setIsValidated(true); return { isValid, newPassword, oldPassword }; }); }; const validateAllFields = async (): Promise<boolean> => { return Promise.all([ oldPasswordInput.current!.checkIsValid(), passwordInput.current!.checkIsValid(), repeatPasswordInput.current!.checkIsValid(), ]).then((res: boolean[]) => { return every(res, (isValid: boolean) => isValid); }); }; return ( <form data-testid="updatePasswordForm" ref={form} className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })} autoComplete="on" noValidate > <InputField ref={oldPasswordInput} type="password" label="Old password" name="oldPassword" invalidText={{ default: 'This field is required', }} autoComplete="password" validateOnBlur required /> <InputField ref={passwordInput} type="password" label="New password" name="password" minLength={6} invalidText={{ default: 'This field is required', customError: 'Insecure password', }} onChange={onPasswordChange} autoComplete="new-password" checkPasswordStrength validateOnChange validateOnBlur required /> <InputField ref={repeatPasswordInput} type="password" label="Confirm new password" labelLegend={<small className="ms-1 fst-italic">(Required)</small>} name="confirmPassword" pattern={password.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} invalidText={{ default: 'This field is required', patternMismatch: "Passwords don't match", }} autoComplete="new-password" validateOnBlur={password.isValid} required /> <div className="mt-4 mb-2"> <button className="btn btn-sm btn-outline-secondary" type="button" disabled={isSending} onClick={submitForm} aria-label="Update password" > {isSending ? ( <> <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" /> <span className="ms-2">Changing password</span> </> ) : ( <div className="d-flex flex-row align-items-center text-uppercase"> <FaPencilAlt className="me-2" /> <div>Change</div> </div> )} </button> </div> </form> ); }; export default UpdatePassword;