diff --git a/src/apis/eduidSecurity.ts b/src/apis/eduidSecurity.ts index be7ef4a7e..42cd6150a 100644 --- a/src/apis/eduidSecurity.ts +++ b/src/apis/eduidSecurity.ts @@ -174,14 +174,13 @@ export const fetchSuggestedPassword = createAsyncThunk< undefined, { dispatch: EduIDAppDispatch; state: EduIDAppRootState } >("chpass/fetchSuggestedPassword", async (args, thunkAPI) => { - return makeSecurityRequest(thunkAPI, "suggested-password") + return makeSecurityRequest(thunkAPI, "change-password/suggested-password") .then((response) => response.payload.suggested_password) .catch((err) => thunkAPI.rejectWithValue(err)); }); /*********************************************************************************************************************/ export interface ChangePasswordPayload { - old_password: string; new_password: string; } @@ -199,7 +198,7 @@ export const changePassword = createAsyncThunk< ChangePasswordPayload, { dispatch: EduIDAppDispatch; state: EduIDAppRootState } >("chpass/changePassword", async (args, thunkAPI) => { - return makeSecurityRequest(thunkAPI, "change-password", args) + return makeSecurityRequest(thunkAPI, "change-password/set-password", args) .then((response) => response.payload) .catch((err) => thunkAPI.rejectWithValue(err)); }); diff --git a/src/components/Common/ConfirmUserInfo.tsx b/src/components/Common/ConfirmUserInfo.tsx new file mode 100644 index 000000000..44653bac5 --- /dev/null +++ b/src/components/Common/ConfirmUserInfo.tsx @@ -0,0 +1,49 @@ +import { idUserEmail, idUserPassword } from "components/Signup/SignupUserCreated"; +import { FormattedMessage } from "react-intl"; + +interface EmailProps { + email?: string; +} + +interface ConfirmUserInfoProps { + readonly email_address: string; + readonly new_password: string; +} + +export const EmailFieldset = ({ email }: EmailProps): JSX.Element => { + return ( +
+ +
+ {email} +
+
+ ); +}; + +export function ConfirmUserInfo(props: ConfirmUserInfoProps) { + return ( +
+ +
+ +
+ + {props.new_password} + +
+ +
+
+ ); +} diff --git a/src/components/Common/CustomInput.tsx b/src/components/Common/CustomInput.tsx index 8b6198d1a..cf98e95df 100644 --- a/src/components/Common/CustomInput.tsx +++ b/src/components/Common/CustomInput.tsx @@ -2,6 +2,8 @@ import { FieldRenderProps } from "react-final-form"; import { Input } from "reactstrap"; import { InputWrapper } from "./InputWrapper"; +type InputType = "text" | "password" | "email"; + export default function CustomInput(props: FieldRenderProps): JSX.Element { // the InputWrapper renders it's children plus a label, helpBlock and any error message from the field validation return ( @@ -16,7 +18,7 @@ const InputElement = (props: FieldRenderProps): JSX.Element => { {errorMsg || submitErrorMsg} + {props.passwordStrengthMeter ? props.passwordStrengthMeter : null} ); } diff --git a/src/components/Common/NewPasswordForm.tsx b/src/components/Common/NewPasswordForm.tsx index d5db90ca4..f7b89aa44 100644 --- a/src/components/Common/NewPasswordForm.tsx +++ b/src/components/Common/NewPasswordForm.tsx @@ -2,7 +2,6 @@ import { ExtraSecurityAlternatives } from "apis/eduidResetPassword"; import CustomInput from "components/Common/CustomInput"; import EduIDButton from "components/Common/EduIDButton"; import { GoBackButton } from "components/ResetPassword/GoBackButton"; -import { FormApi, SubmissionErrors } from "final-form"; import { emptyStringPattern } from "helperFunctions/validation/regexPatterns"; import { Field as FinalField, Form as FinalForm } from "react-final-form"; import { FormattedMessage } from "react-intl"; @@ -17,12 +16,14 @@ interface NewPasswordFormProps { readonly goBack?: () => void; readonly extra_security?: ExtraSecurityAlternatives; readonly suggested_password: string | undefined; - readonly submitNewPasswordForm: ( - values: NewPasswordFormData, - form: FormApi>, - callback?: ((errors?: SubmissionErrors) => void) | undefined - ) => void | Promise; + readonly submitNewPasswordForm: any; + // submitNewPasswordForm: ( + // values: NewPasswordFormData, + // form: FormApi>, + // callback?: ((errors?: SubmissionErrors) => void) | undefined + // ) => void | Promise; readonly submitButtonText: React.ReactChild; + readonly handleCancel?: (event: React.MouseEvent) => void; } export function NewPasswordForm(props: NewPasswordFormProps): JSX.Element { @@ -61,6 +62,11 @@ export function NewPasswordForm(props: NewPasswordFormProps): JSX.Element { {props.extra_security && Object.keys(props.extra_security).length > 0 && ( )} + {props.handleCancel && ( + + + + )} {props.submitButtonText} diff --git a/src/components/Common/NewPasswordInput.tsx b/src/components/Common/NewPasswordInput.tsx new file mode 100644 index 000000000..f24b2caa5 --- /dev/null +++ b/src/components/Common/NewPasswordInput.tsx @@ -0,0 +1,12 @@ +import { FieldRenderProps } from "react-final-form"; +import { InputWrapper } from "./InputWrapper"; +import { PasswordInputElement } from "./PasswordInput"; + +export default function NewPasswordInput(props: FieldRenderProps): JSX.Element { + // the InputWrapper renders it's children plus a label, helpBlock and any error message from the field validation + return ( + + + + ); +} diff --git a/src/components/Common/PasswordInput.tsx b/src/components/Common/PasswordInput.tsx index ad8ae4fb4..452228732 100644 --- a/src/components/Common/PasswordInput.tsx +++ b/src/components/Common/PasswordInput.tsx @@ -50,7 +50,7 @@ export function WrappedPasswordInput(props: FieldRenderProps): JSX.Eleme * @param props * @returns */ -function PasswordInputElement(props: InputProps): JSX.Element { +export function PasswordInputElement(props: InputProps): JSX.Element { const [showPassword, setShowPassword] = useState(false); function toggleShowPassword() { diff --git a/src/components/Common/PasswordStrengthMeter.tsx b/src/components/Common/PasswordStrengthMeter.tsx index 73c7ed6ea..c43a735d8 100644 --- a/src/components/Common/PasswordStrengthMeter.tsx +++ b/src/components/Common/PasswordStrengthMeter.tsx @@ -21,7 +21,6 @@ function PasswordStrengthMeter(props: PasswordStrengthMeterProps) { const emails = useAppSelector((state) => state.emails.emails); const [pwScore, setPwScore] = useState(0); const intl = useIntl(); - const pwStrengthMessages = ["pwfield.terrible", "pwfield.bad", "pwfield.weak", "pwfield.good", "pwfield.strong"]; useEffect(() => { @@ -53,10 +52,10 @@ function PasswordStrengthMeter(props: PasswordStrengthMeterProps) { return ( -
- {intl.formatMessage({ id: pwStrengthMessages[pwScore] })} + {props.password !== undefined && {intl.formatMessage({ id: pwStrengthMessages[pwScore] })}}
+
); } diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index b4a89d36e..0b9659626 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -1,17 +1,40 @@ -import { fetchSuggestedPassword } from "apis/eduidSecurity"; +import { changePassword, fetchSuggestedPassword } from "apis/eduidSecurity"; +import Splash from "components/Common/Splash"; import { useAppDispatch, useAppSelector } from "eduid-hooks"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; +import { Form as FinalForm, FormRenderProps } from "react-final-form"; import { FormattedMessage, useIntl } from "react-intl"; -import ChangePasswordForm from "./ChangePasswordForm"; +import { useNavigate } from "react-router-dom"; +import ChangePasswordCustomForm from "./ChangePasswordCustom"; +import ChangePasswordSuggestedForm from "./ChangePasswordSuggested"; +import { ChangePasswordSwitchToggle } from "./ChangePasswordSwitchToggle"; // exported for use in tests export const finish_url = "/profile/security"; -function ChangePassword() { +export interface ChangePasswordFormProps { + finish_url: string; // URL to direct browser to when user cancels password change, or completes it +} + +export interface ChangePasswordChildFormProps { + formProps: FormRenderProps; + handleCancel?: (event: React.MouseEvent) => void; +} + +interface ChangePasswordFormData { + custom?: string; // used with custom password + score?: number; // used with custom password + suggested?: string; // used with suggested password +} + +export function ChangePassword() { const suggested_password = useAppSelector((state) => state.chpass.suggested_password); const is_app_loaded = useAppSelector((state) => state.config.is_app_loaded); const dispatch = useAppDispatch(); const intl = useIntl(); + const suggested = useAppSelector((state) => state.chpass.suggested_password); + const [renderSuggested, setRenderSuggested] = useState(true); // toggle display of custom or suggested password forms + const navigate = useNavigate(); useEffect(() => { document.title = intl.formatMessage({ @@ -22,21 +45,101 @@ function ChangePassword() { useEffect(() => { if (is_app_loaded && suggested_password === undefined) { - // call fetchSuggestedPassword once state.config.security_service_url is initialised - dispatch(fetchSuggestedPassword()); + handleSuggestedPassword(); } }, [suggested_password, is_app_loaded]); + async function handleSuggestedPassword() { + const response = await dispatch(fetchSuggestedPassword()); + if (fetchSuggestedPassword.rejected.match(response)) { + navigate(finish_url); + } + } + + async function handleSubmitPasswords(values: ChangePasswordFormData) { + // Use the right form field for the currently displayed password mode + const newPassword = renderSuggested ? values.suggested : values.custom; + + // Callback from sub-component when the user clicks on the button to change password + if (newPassword) { + const response = await dispatch(changePassword({ new_password: newPassword })); + if (changePassword.fulfilled.match(response)) { + navigate("/profile/chpass/success", { + state: newPassword, + }); + } + } + } + + function handleCancel(event: React.MouseEvent) { + // Callback from sub-component when the user clicks on the button to abort changing password + event.preventDefault(); + + navigate(finish_url); + } + + const initialValues = { suggested }; + + function handleSwitchChange() { + setRenderSuggested(!renderSuggested); + } + return ( - -

- -

-
- -
-
+ + onSubmit={handleSubmitPasswords} + initialValues={initialValues} + render={(formProps) => { + const child_props: ChangePasswordChildFormProps = { formProps }; + + return ( + + {renderSuggested ? ( +
+

+ +

+
+

+ +

+
+
+ ) : ( +
+

+ +

+
+

+ +

+
+
+ )} + + {renderSuggested ? ( + + ) : ( + + )} +
+ ); + }} + /> ); } - -export const ChangePasswordContainer = ChangePassword; diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index 63bcc9237..8b122614a 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -1,128 +1,148 @@ -import TextInput from "components/Common/EduIDTextInput"; -import PasswordStrengthMeter, { PasswordStrengthData } from "components/Common/PasswordStrengthMeter"; -import { useState } from "react"; -import { Field as FinalField } from "react-final-form"; -import { FormattedMessage } from "react-intl"; -import { ChangePasswordChildFormProps } from "./ChangePasswordForm"; +import EduIDButton from "components/Common/EduIDButton"; +import NewPasswordInput from "components/Common/NewPasswordInput"; +import PasswordStrengthMeter from "components/Common/PasswordStrengthMeter"; +import { emptyStringPattern } from "helperFunctions/validation/regexPatterns"; +import { Field as FinalField, Form as FinalForm } from "react-final-form"; +import { FormattedMessage, useIntl } from "react-intl"; +import { ChangePasswordChildFormProps } from "./ChangePassword"; // eslint-disable-next-line @typescript-eslint/no-empty-interface interface ChangePasswordCustomFormProps extends ChangePasswordChildFormProps {} export default function ChangePasswordCustomForm(props: ChangePasswordCustomFormProps) { - const [passwordData, setPasswordData] = useState({}); + const intl = useIntl(); - function updatePasswordData(value: PasswordStrengthData) { + const new_password_placeholder = intl.formatMessage({ + id: "placeholder.new_password_placeholder", + defaultMessage: "enter new password", + description: "placeholder text for new password", + }); + + const repeat_new_password_placeholder = intl.formatMessage({ + id: "placeholder.repeat_new_password_placeholder", + defaultMessage: "repeat new password", + description: "placeholder text for repeat new password", + }); + + function updatePasswordData() { // This function is called when the password strength meter has calculated password strength // on the current value in the form. We need to trigger validation of the field again at this // point, since validation uses this calculated value (and will already have executed when we // get here). - setPasswordData(value); props.formProps.form.change("custom", props.formProps.values.custom); } - // Form field validators - const required = (value?: string) => (value ? undefined : "required"); - - function strongEnough(value?: string): string | undefined { - // check that the custom password is strong enough, using a score computed in the - // PasswordStrengthMeter component. - if (!value) { - return "required"; - } - if (passwordData.isTooWeak !== false) { - return "chpass.low-password-entropy"; + function validateNewPassword(values: { custom?: string; repeat?: string }) { + const errors: { custom?: string; repeat?: string } = {}; + if (values !== undefined) { + (["custom", "repeat"] as Array).forEach((inputName) => { + if (!values[inputName] || emptyStringPattern.test(values[inputName] as string)) { + errors[inputName] = "required"; + } else if (values["custom"]?.replace(/\s/g, "") !== values["repeat"]?.replace(/\s/g, "")) { + // Remove whitespace from both passwords before comparing + errors["repeat"] = "chpass.different-repeat"; + } + }); } - } - function mustMatch(value?: string): string | undefined { - // validate that the repeated password is the same as the first one (called 'custom') - if (!value) { - return "required"; - } - if (value !== props.formProps.values.custom) { - return "chpass.different-repeat"; - } + return errors; } return ( -
-
- } - validate={required} - autoComplete="current-password" - /> -
-
- -
    - {[ - , - , - , - , - ].map((list) => { - return
  • {list}
  • ; - })} -
-
+ + onSubmit={props.formProps.handleSubmit} + validate={validateNewPassword} + render={(formProps) => { + return ( + +
+ +
    + {[ + , + , + , + , + ].map((list) => { + return
  • {list}
  • ; + })} +
+
-
-
- - } - helpBlock={ - - } - id="custom-password-field" - validate={strongEnough} - autoComplete="new-password" - /> - - } - validate={mustMatch} - /> -
-
- +
+ + } + passwordStrengthMeter={ + + } + id="custom-password-field" + autoComplete="new-password" + required={true} + placeHolder={new_password_placeholder} + /> + + } + required={true} + placeHolder={repeat_new_password_placeholder} + /> +
+
+ + + + + + +
+ + ); + }} + /> ); } diff --git a/src/components/Dashboard/ChangePasswordDisplay.tsx b/src/components/Dashboard/ChangePasswordDisplay.tsx index 84e80581a..479c19eeb 100644 --- a/src/components/Dashboard/ChangePasswordDisplay.tsx +++ b/src/components/Dashboard/ChangePasswordDisplay.tsx @@ -14,7 +14,7 @@ function ChangePasswordDisplay(props: ChangePasswordDisplayProps) { function handleAcceptModal() { const chpassURL = config.authn_service_url + "chpass"; - // the "chpass" path will route to the ChangePasswordContainer when we get back + // the "chpass" path will route to the ChangePassword when we get back const nextURL = config.dashboard_link + "chpass"; const url = chpassURL + "?next=" + encodeURIComponent(nextURL); window.location.assign(url); diff --git a/src/components/Dashboard/ChangePasswordForm.tsx b/src/components/Dashboard/ChangePasswordForm.tsx deleted file mode 100644 index d3ed187f8..000000000 --- a/src/components/Dashboard/ChangePasswordForm.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { changePassword } from "apis/eduidSecurity"; -import EduIDButton from "components/Common/EduIDButton"; -import { useAppDispatch, useAppSelector } from "eduid-hooks"; -import React, { useState } from "react"; -import { Form as FinalForm, FormRenderProps } from "react-final-form"; -import { FormattedMessage } from "react-intl"; -import { useNavigate } from "react-router-dom"; -import { ButtonGroup } from "reactstrap"; -import ChangePasswordCustomForm from "./ChangePasswordCustom"; -import ChangePasswordSuggestedForm from "./ChangePasswordSuggested"; - -export interface ChangePasswordFormProps { - finish_url: string; // URL to direct browser to when user cancels password change, or completes it -} - -// These are the props we pass to the sub-components with the different forms -export interface ChangePasswordChildFormProps { - formProps: FormRenderProps; -} - -interface ChangePasswordFormData { - old?: string; // used by both modes - custom?: string; // used with custom password - score?: number; // used with custom password - suggested?: string; // used with suggested password -} - -function ChangePasswordForm(props: ChangePasswordFormProps) { - const suggested = useAppSelector((state) => state.chpass.suggested_password); - const [renderSuggested, setRenderSuggested] = useState(true); // toggle display of custom or suggested password forms - const dispatch = useAppDispatch(); - const navigate = useNavigate(); - - function togglePasswordType() { - // Toggle between rendering the suggested password form, or the custom password form - setRenderSuggested(!renderSuggested); - } - - async function handleSubmitPasswords(values: ChangePasswordFormData) { - // Use the right form field for the currently displayed password mode - const newPassword = renderSuggested ? values.suggested : values.custom; - // Callback from sub-component when the user clicks on the button to change password - if (values.old && newPassword) { - const response = await dispatch(changePassword({ old_password: values.old, new_password: newPassword })); - if (changePassword.fulfilled.match(response)) { - navigate(props.finish_url); - } - } - } - - function handleCancel(event: React.MouseEvent) { - // Callback from sub-component when the user clicks on the button to abort changing password - event.preventDefault(); - // TODO: should clear passwords from form to avoid browser password manager asking user to save the password - navigate(props.finish_url); - } - - const initialValues = { suggested }; - - return ( - - onSubmit={handleSubmitPasswords} - initialValues={initialValues} - render={(formProps) => { - const child_props: ChangePasswordChildFormProps = { formProps }; - - return ( - - {renderSuggested ? ( - - ) : ( - - )} - -
- - - {renderSuggested ? ( - - ) : ( - - )} - - -
-
- - - - - - -
-
- ); - }} - /> - ); -} - -export default ChangePasswordForm; diff --git a/src/components/Dashboard/ChangePasswordSuccess.tsx b/src/components/Dashboard/ChangePasswordSuccess.tsx new file mode 100644 index 000000000..63a7fa278 --- /dev/null +++ b/src/components/Dashboard/ChangePasswordSuccess.tsx @@ -0,0 +1,39 @@ +import { ConfirmUserInfo } from "components/Common/ConfirmUserInfo"; +import EduIDButton from "components/Common/EduIDButton"; +import { useAppSelector } from "eduid-hooks"; +import { FormattedMessage } from "react-intl"; +import { useLocation } from "react-router-dom"; +import { finish_url } from "./ChangePassword"; + +export function ChangePasswordSuccess(): JSX.Element { + const emails = useAppSelector((state) => state.emails.emails); + const location = useLocation(); + const password = location.state; + + return ( +
+
+

+ +

+
+

+ +

+
+
+ mail.primary)[0].email} new_password={password} /> +
+ + + +
+ + ); +} diff --git a/src/components/Dashboard/ChangePasswordSuggested.tsx b/src/components/Dashboard/ChangePasswordSuggested.tsx index a35a7ec80..219106072 100644 --- a/src/components/Dashboard/ChangePasswordSuggested.tsx +++ b/src/components/Dashboard/ChangePasswordSuggested.tsx @@ -1,39 +1,35 @@ -import TextInput from "components/Common/EduIDTextInput"; -import { Field as FinalField } from "react-final-form"; +import { CopyToClipboard } from "components/Common/CopyToClipboard"; +import { NewPasswordForm } from "components/Common/NewPasswordForm"; +import { useAppSelector } from "eduid-hooks"; +import React, { useRef } from "react"; import { FormattedMessage } from "react-intl"; -import { ChangePasswordChildFormProps } from "./ChangePasswordForm"; +import { ChangePasswordChildFormProps } from "./ChangePassword"; export default function ChangePasswordSuggestedForm(props: ChangePasswordChildFormProps) { - // Form field validator - const required = (value: string) => (value ? undefined : "required"); + const ref = useRef(null); + const suggested_password = useAppSelector((state) => state.chpass.suggested_password); return ( -
-
- - name="old" - component={TextInput} - componentClass="input" - type="password" - id="old-password-field" - label={} - validate={required} - autoComplete="current-password" + +
+ + -
-
- - name="suggested" - component={TextInput} - componentClass="input" - type="text" - id="suggested-password-field" - className="suggested-password" - label={} - disabled={true} - autoComplete="new-password" - /> -
-
+ + + } + handleCancel={props.handleCancel} + /> + ); } diff --git a/src/components/Dashboard/ChangePasswordSwitchToggle.tsx b/src/components/Dashboard/ChangePasswordSwitchToggle.tsx new file mode 100644 index 000000000..6a0a99ca9 --- /dev/null +++ b/src/components/Dashboard/ChangePasswordSwitchToggle.tsx @@ -0,0 +1,30 @@ +import { FormattedMessage } from "react-intl"; + +export function ChangePasswordSwitchToggle(props: { + readonly handleSwitchChange: (event: React.ChangeEvent) => void; + readonly renderSuggested: boolean; +}) { + return ( +
+
+ +
+

+ +

+
+ ); +} diff --git a/src/components/Dashboard/NinForm.tsx b/src/components/Dashboard/NinForm.tsx index 796ff37a4..99b33208f 100644 --- a/src/components/Dashboard/NinForm.tsx +++ b/src/components/Dashboard/NinForm.tsx @@ -67,7 +67,7 @@ function NinForm(): JSX.Element { render={({ handleSubmit, pristine, invalid }) => { return (
-
+
} /> } /> } /> - } /> + } /> + } /> } /> {/* Navigates for old paths. TODO: redirect in backend server instead */} } /> diff --git a/src/components/ResetPassword/SetNewPassword.tsx b/src/components/ResetPassword/SetNewPassword.tsx index 7dfa4c5b8..93278cd9a 100644 --- a/src/components/ResetPassword/SetNewPassword.tsx +++ b/src/components/ResetPassword/SetNewPassword.tsx @@ -4,6 +4,7 @@ import { postSetNewPasswordExtraSecurityPhone, postSetNewPasswordExtraSecurityToken, } from "apis/eduidResetPassword"; +import { ConfirmUserInfo } from "components/Common/ConfirmUserInfo"; import { CopyToClipboard } from "components/Common/CopyToClipboard"; import EduIDButton from "components/Common/EduIDButton"; import { NewPasswordForm, NewPasswordFormData } from "components/Common/NewPasswordForm"; @@ -100,7 +101,7 @@ export function SetNewPassword(): JSX.Element | null {

-
+
@@ -150,32 +151,7 @@ export function ResetPasswordSuccess(): JSX.Element {

-
-
- -
- {email_address} -
-
-
- -
- - {new_password} - -
- -
-
+
diff --git a/src/components/Signup/SignupApp.tsx b/src/components/Signup/SignupApp.tsx index 100d78489..a463075d6 100644 --- a/src/components/Signup/SignupApp.tsx +++ b/src/components/Signup/SignupApp.tsx @@ -14,7 +14,6 @@ import { SignupConfirmPassword, SignupUserCreated } from "./SignupUserCreated"; export function SignupApp(): JSX.Element { const signupContext = useContext(SignupGlobalStateContext); const [state] = useActor(signupContext.signupService); - const intl = useIntl(); useEffect(() => { diff --git a/src/components/Signup/SignupUserCreated.tsx b/src/components/Signup/SignupUserCreated.tsx index d0d5301e6..2ff8b2c50 100644 --- a/src/components/Signup/SignupUserCreated.tsx +++ b/src/components/Signup/SignupUserCreated.tsx @@ -1,4 +1,5 @@ import { createUserRequest } from "apis/eduidSignup"; +import { ConfirmUserInfo, EmailFieldset } from "components/Common/ConfirmUserInfo"; import { CopyToClipboard } from "components/Common/CopyToClipboard"; import EduIDButton from "components/Common/EduIDButton"; import { NewPasswordForm, NewPasswordFormData } from "components/Common/NewPasswordForm"; @@ -13,23 +14,6 @@ export const idUserEmail = "user-email"; export const idUserPassword = "user-password"; export const idFinishedButton = "finished-button"; -interface EmailProps { - email?: string; -} - -const EmailFieldset = ({ email }: EmailProps): JSX.Element => { - return ( -
- -
- {email} -
-
- ); -}; - export function SignupConfirmPassword() { const dispatch = useAppDispatch(); const signupContext = useContext(SignupGlobalStateContext); @@ -70,14 +54,6 @@ export function SignupConfirmPassword() {
- {/*
- -
- {signupState?.email.address} -
-
*/}
-
- -
- -
- - {formatPassword(signupState?.credentials.password)} - -
- -
-
- +
diff --git a/src/styles/_ChangePassword.scss b/src/styles/_ChangePassword.scss index 436f549e1..7179ad7cf 100644 --- a/src/styles/_ChangePassword.scss +++ b/src/styles/_ChangePassword.scss @@ -8,11 +8,10 @@ margin-bottom: 1rem; } -#changePasswordDialog { - // background: yellow; - & fieldset { - margin-bottom: 1rem; - } +fieldset.toggle-change-password-options { + padding-bottom: 2rem; + margin-bottom: 2rem; + border-bottom: 1px solid #d8d8d8; } #password-custom-help { @@ -22,7 +21,55 @@ meter { width: 100%; - // background: yellow; + height: 2.5rem; +} + +meter[value="1"]::-webkit-meter-optimum-value { + background: red; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="2"]::-webkit-meter-optimum-value { + background: yellow; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="3"]::-webkit-meter-optimum-value { + background: orange; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="4"]::-webkit-meter-optimum-value { + background: green; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} + +/* Gecko based browsers */ +meter[value="1"]::-moz-meter-bar { + background: red; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="2"]::-moz-meter-bar { + background: yellow; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="3"]::-moz-meter-bar { + background: orange; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="4"]::-moz-meter-bar { + background: green; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} + +.form-field-error-area small.text-muted { + font-size: 0.9rem !important; + color: #d2143a !important; } @media (max-width: 375px) { diff --git a/src/styles/_ResetPassword.scss b/src/styles/_ResetPassword.scss index 07ea14473..0b6082e37 100644 --- a/src/styles/_ResetPassword.scss +++ b/src/styles/_ResetPassword.scss @@ -152,7 +152,7 @@ a#resend-phone:not(.button-active) { } } -.reset-password-input { +.copy-password-input { position: relative; // not needed since Splash is relative but best leave for safety. } diff --git a/src/styles/_anti-bootstrap.scss b/src/styles/_anti-bootstrap.scss index 0398f2e9f..37f47cb5e 100644 --- a/src/styles/_anti-bootstrap.scss +++ b/src/styles/_anti-bootstrap.scss @@ -30,10 +30,6 @@ input { small { font-size: 100% !important; - &.text-muted { - color: $txt-black !important; - } - &.form-text { margin-top: 0 !important; } diff --git a/src/styles/_inputs.scss b/src/styles/_inputs.scss index c00d3f218..8982b9df5 100644 --- a/src/styles/_inputs.scss +++ b/src/styles/_inputs.scss @@ -157,6 +157,12 @@ input[readonly]:focus-visible { } } +fieldset.change-password-custom-inputs { + span.input-validate-error { + position: relative !important; + } +} + // login username-pw / reset-password set-new-password .password-input { display: flex; diff --git a/src/tests/ChangePasswordForm-test.tsx b/src/tests/ChangePasswordForm-test.tsx index 3f06f91f2..fe20ed1e3 100644 --- a/src/tests/ChangePasswordForm-test.tsx +++ b/src/tests/ChangePasswordForm-test.tsx @@ -1,38 +1,38 @@ -import { finish_url } from "components/Dashboard/ChangePassword"; -import ChangePasswordForm from "components/Dashboard/ChangePasswordForm"; -import { fireEvent, render, screen, waitFor } from "./helperFunctions/DashboardTestApp-rtl"; +import { ChangePassword } from "components/Dashboard/ChangePassword"; +import { initialState as configInitialState } from "slices/IndexConfig"; -test("renders ChangePasswordForm, suggested password value is field in suggested-password-field", () => { - render(); +import { fireEvent, render, screen, waitFor } from "./helperFunctions/DashboardTestApp-rtl"; - const oldPasswordInput = screen.getByLabelText(/Current password/i) as HTMLInputElement; - expect(oldPasswordInput.value).toBe(""); +const suggestPassword = "test-password"; - const suggestedPasswordInput = screen.getByLabelText(/Suggested password/i) as HTMLInputElement; +test("renders ChangePasswordForm, suggested password value is field in suggested-password-field", () => { + render(, { + state: { + config: { ...configInitialState, is_app_loaded: true }, + chpass: { + suggested_password: suggestPassword, + }, + }, + }); + expect(screen.getByRole("heading")).toHaveTextContent(/^Change password: Suggested password/); + const suggestedPasswordInput = screen.getByLabelText(/Repeat new password/i) as HTMLInputElement; expect(suggestedPasswordInput.value).toBeDefined(); }); -test("save button will be enabled once current password field is filled", () => { - render(); - - const input = screen.getByLabelText(/Current password/i) as HTMLInputElement; - // change password save button is initially disabled - const savePasswordButton = screen.getByRole("button", { name: /save/i }); - expect(savePasswordButton).toBeDisabled(); - - fireEvent.change(input, { target: { value: "current password" } }); - expect(input.value).toBe("current password"); - - expect(savePasswordButton).toBeEnabled(); -}); - test("renders custom password form after clicking do not want a suggested password", async () => { - render(); - const customPasswordButton = screen.getByRole("button", { name: /I don't want a suggested password/i }); + render(, { + state: { + config: { ...configInitialState, is_app_loaded: true }, + chpass: { + suggested_password: suggestPassword, + }, + }, + }); + const customPasswordButton = screen.getByRole("checkbox", { name: /Create a custom password?/i }); expect(customPasswordButton).toBeInTheDocument(); fireEvent.click(customPasswordButton); - expect(screen.getByText(/Suggest a password for me/i)).toBeInTheDocument(); + expect(customPasswordButton).toBeChecked(); const newPasswordInput = screen.getByLabelText(/Enter new password/i) as HTMLInputElement; const repeatNewPasswordInput = screen.getByLabelText(/Repeat new password/i) as HTMLInputElement;