diff --git a/src/locales/en/en.json b/src/locales/en/en.json index f7f8fa9af..b858bc4ba 100644 --- a/src/locales/en/en.json +++ b/src/locales/en/en.json @@ -183,7 +183,7 @@ }, "button": { "continue": "Create Password", - "skip": "Continue without setting a password" + "skip": "Skip" }, "alert": { "text": "You have chosen to skip creating a password. If you change your mind later, you will still be able to create a password.", diff --git a/src/routes/nextRoute/nextRoute.test.ts b/src/routes/nextRoute/nextRoute.test.ts index ee3c72c77..0f6683639 100644 --- a/src/routes/nextRoute/nextRoute.test.ts +++ b/src/routes/nextRoute/nextRoute.test.ts @@ -66,7 +66,7 @@ describe("NextRoute", () => { const result = getNextOnboardingRoute(data as DataProps); expect(result).toEqual({ - pathname: RoutePath.GENERATE_SEED_PHRASE, + pathname: RoutePath.CREATE_PASSWORD, }); }); @@ -86,7 +86,7 @@ describe("NextRoute", () => { const result = getNextSetPasscodeRoute(storeMock); expect(result).toEqual({ - pathname: RoutePath.GENERATE_SEED_PHRASE, + pathname: RoutePath.CREATE_PASSWORD, }); }); @@ -115,7 +115,7 @@ describe("NextRoute", () => { const result = getNextVerifySeedPhraseRoute(); expect(result).toEqual({ - pathname: RoutePath.CREATE_PASSWORD, + pathname: RoutePath.TABS_MENU, }); }); }); @@ -162,7 +162,7 @@ describe("getNextRoute", () => { }); expect(result.nextPath).toEqual({ - pathname: RoutePath.GENERATE_SEED_PHRASE, + pathname: RoutePath.CREATE_PASSWORD, }); storeMock.stateCache.authentication.passcodeIsSet = false; @@ -195,7 +195,7 @@ describe("getNextRoute", () => { const result = getNextSetPasscodeRoute(storeMock); expect(result).toEqual({ - pathname: RoutePath.GENERATE_SEED_PHRASE, + pathname: RoutePath.CREATE_PASSWORD, }); }); }); diff --git a/src/routes/nextRoute/nextRoute.ts b/src/routes/nextRoute/nextRoute.ts index 40aa5a2ee..3031221ee 100644 --- a/src/routes/nextRoute/nextRoute.ts +++ b/src/routes/nextRoute/nextRoute.ts @@ -41,7 +41,9 @@ const getNextRootRoute = (store: StoreState) => { const getNextOnboardingRoute = (data: DataProps) => { let path; if (data.store.stateCache.authentication.passcodeIsSet) { - path = RoutePath.GENERATE_SEED_PHRASE; + path = data.store.stateCache.authentication.passwordIsSet + ? RoutePath.GENERATE_SEED_PHRASE + : RoutePath.CREATE_PASSWORD; } else { path = RoutePath.SET_PASSCODE; } @@ -74,7 +76,7 @@ const getNextSetPasscodeRoute = (store: StoreState) => { const nextPath: string = seedPhraseIsSet ? RoutePath.TABS_MENU - : RoutePath.GENERATE_SEED_PHRASE; + : RoutePath.CREATE_PASSWORD; return { pathname: nextPath }; }; @@ -99,7 +101,7 @@ const getNextGenerateSeedPhraseRoute = () => { }; const getNextVerifySeedPhraseRoute = () => { - const nextPath = RoutePath.CREATE_PASSWORD; + const nextPath = RoutePath.TABS_MENU; return { pathname: nextPath }; }; @@ -115,7 +117,7 @@ const updateStoreCurrentRoute = (data: DataProps) => { }; const getNextCreatePasswordRoute = () => { - return { pathname: RoutePath.TABS_MENU }; + return { pathname: RoutePath.GENERATE_SEED_PHRASE }; }; const updateStoreAfterCreatePassword = (data: DataProps) => { const skipped = data.state?.skipped; @@ -173,7 +175,7 @@ const nextRoute: Record = { updateRedux: [updateStoreSetSeedPhrase], }, [RoutePath.VERIFY_SEED_PHRASE]: { - nextPath: (data: DataProps) => getNextVerifySeedPhraseRoute(), + nextPath: () => getNextVerifySeedPhraseRoute(), updateRedux: [updateStoreAfterVerifySeedPhraseRoute, clearSeedPhraseCache], }, [RoutePath.CREATE_PASSWORD]: { diff --git a/src/ui/components/CreateIdentifier/CreateIdentifier.scss b/src/ui/components/CreateIdentifier/CreateIdentifier.scss index 419f936ac..4cf44e281 100644 --- a/src/ui/components/CreateIdentifier/CreateIdentifier.scss +++ b/src/ui/components/CreateIdentifier/CreateIdentifier.scss @@ -29,7 +29,7 @@ } .type-input-title, - ion-item.input-item ion-label[position="stacked"] { + ion-item.custom-input ion-label[position="stacked"] { font-size: 1rem; font-weight: 500; line-height: 1.375rem; @@ -37,11 +37,11 @@ transform: none; } - ion-item.input-item .input-line ion-input input::placeholder { + ion-item.custom-input .input-line ion-input input::placeholder { font-size: 1rem; } - .hide-title ion-item.input-item ion-label[position="stacked"] { + .hide-title ion-item.custom-input ion-label[position="stacked"] { opacity: 0; } diff --git a/src/ui/components/CustomInput/CustomInput.scss b/src/ui/components/CustomInput/CustomInput.scss index 56bb8df98..bd41b6d29 100644 --- a/src/ui/components/CustomInput/CustomInput.scss +++ b/src/ui/components/CustomInput/CustomInput.scss @@ -1,4 +1,4 @@ -ion-item.input-item { +ion-item.custom-input { --background: var(--ion-color-light); --border-width: 0; --inner-border-width: 0; @@ -7,27 +7,38 @@ ion-item.input-item { ion-label[position="stacked"] { margin-bottom: 0.625rem; - .input-item-optional { + + .custom-input-optional { margin-left: 0.25rem; opacity: 50%; } } + &.error .input-line { + border-color: var(--ion-color-danger); + } + .input-line { display: flex; width: 100%; border: 1px solid var(--ion-color-dark-grey); border-radius: 8px; + &:focus-within { + border-color: var(--ion-color-secondary); + } + + .input-wrapper { + justify-content: center; + } + ion-input { - input { - --padding-top: 0.875rem; - --padding-bottom: 0.875rem; - --padding-start: 1.25rem; - } + --padding-top: 0.875rem; + --padding-bottom: 0.875rem; + --padding-start: 1.25rem; - &.has-focus { - border-color: var(--ion-color-secondary); + .label-text-wrapper { + display: none; } } @@ -43,8 +54,36 @@ ion-item.input-item { &::part(native) { padding: 0; } + ion-icon { - font-size: 1.85em !important; + font-size: 1.85em; + } + } + } + + @media screen and (min-width: 250px) and (max-width: 370px) { + ion-label[position="stacked"] { + font-size: 0.8rem; + line-height: 1rem; + } + + .input-line { + height: 2.725rem; + + ion-input { + --padding-top: 0.35rem; + --padding-bottom: 0.35rem; + --padding-start: 0.8rem; + min-height: 100%; + font-size: 0.8rem; + + .native-wrapper { + flex-grow: 0; + } + } + + ion-button ion-icon { + font-size: 1rem; } } } diff --git a/src/ui/components/CustomInput/CustomInput.tsx b/src/ui/components/CustomInput/CustomInput.tsx index 9be856e7e..fda3f9f87 100644 --- a/src/ui/components/CustomInput/CustomInput.tsx +++ b/src/ui/components/CustomInput/CustomInput.tsx @@ -15,6 +15,7 @@ const CustomInput = ({ onChangeFocus, optional, value, + error, }: CustomInputProps) => { const [hidden, setHidden] = useState(hiddenInput); @@ -24,11 +25,11 @@ const CustomInput = ({ } }; return ( - + {title} {optional && ( - + {i18n.t("custominput.optional")} )} @@ -37,10 +38,12 @@ const CustomInput = ({ onChangeInput(`${e.target.value ?? ""}`)} + onIonInput={(e) => onChangeInput(e.target.value as string)} onIonFocus={() => handleFocus(true)} onIonBlur={() => handleFocus(false)} value={value} diff --git a/src/ui/components/CustomInput/CustomInput.types.ts b/src/ui/components/CustomInput/CustomInput.types.ts index 079b951a9..e8479a52e 100644 --- a/src/ui/components/CustomInput/CustomInput.types.ts +++ b/src/ui/components/CustomInput/CustomInput.types.ts @@ -5,11 +5,12 @@ interface CustomInputProps { title?: string; autofocus?: boolean; placeholder?: string; - hiddenInput: boolean; + hiddenInput?: boolean; value: string; onChangeInput: (text: string) => void; onChangeFocus?: Dispatch>; optional?: boolean; + error?: boolean; } export type { CustomInputProps }; diff --git a/src/ui/components/ErrorMessage/ErrorMessage.types.ts b/src/ui/components/ErrorMessage/ErrorMessage.types.ts index ed7c9c938..a551cb1d8 100644 --- a/src/ui/components/ErrorMessage/ErrorMessage.types.ts +++ b/src/ui/components/ErrorMessage/ErrorMessage.types.ts @@ -1,6 +1,6 @@ interface ErrorMessageProps { message: string | undefined; - timeout: boolean; + timeout?: boolean; } export type { ErrorMessageProps }; diff --git a/src/ui/components/IdentifierOptions/IdentifierOptions.scss b/src/ui/components/IdentifierOptions/IdentifierOptions.scss index 3b1673fed..592b26486 100644 --- a/src/ui/components/IdentifierOptions/IdentifierOptions.scss +++ b/src/ui/components/IdentifierOptions/IdentifierOptions.scss @@ -7,7 +7,7 @@ color: var(--ion-color-primary); } - .input-item { + .custom-input { ion-label, .theme-input-title { font-size: 1rem; diff --git a/src/ui/components/PasswordValidation/PasswordValidation.scss b/src/ui/components/PasswordValidation/PasswordValidation.scss new file mode 100644 index 000000000..3b753442e --- /dev/null +++ b/src/ui/components/PasswordValidation/PasswordValidation.scss @@ -0,0 +1,39 @@ +.password-validation { + padding-bottom: 0; + background-color: var(--ion-color-light); + + ion-item { + --background: var(--ion-color-light); + .password-criteria-icon { + font-size: 1.1rem; + padding: 0.2rem; + margin-right: 0.5rem; + border-radius: 1.5rem; + &.fails { + color: var(--ion-color-secondary); + background: var(--ion-color-light-grey); + } + &.pass { + color: white; + background: var(--ion-color-green); + } + } + + &::part(native) { + padding: 0; + } + } + + @media screen and (min-width: 250px) and (max-width: 370px) { + ion-label { + font-size: 0.8rem; + line-height: 1rem; + } + + ion-icon, + ion-label { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + } + } +} diff --git a/src/ui/components/PasswordValidation/PasswordValidation.test.tsx b/src/ui/components/PasswordValidation/PasswordValidation.test.tsx new file mode 100644 index 000000000..bab4d1b21 --- /dev/null +++ b/src/ui/components/PasswordValidation/PasswordValidation.test.tsx @@ -0,0 +1,74 @@ +import { render } from "@testing-library/react"; +import { PasswordValidation } from "../../components/PasswordValidation"; + +describe("Create Password Page", () => { + test("validates password correctly", () => { + const { container } = render(); + const regexConditions = container.getElementsByClassName( + "password-criteria-icon" + ); + for (let i = 0; i < regexConditions.length; i++) { + expect(regexConditions[i]).toHaveClass("pass"); + } + }); + + test("validates password length correctly", () => { + const { container } = render(); + const regexConditions = container.getElementsByClassName( + "password-criteria-icon" + ); + expect(regexConditions[0]).toHaveClass("pass"); + }); + + test("validates password is too short", () => { + const { container } = render(); + const regexConditions = container.getElementsByClassName( + "password-criteria-icon" + ); + expect(regexConditions[0]).toHaveClass("fails"); + }); + + test("validates password is too long", () => { + const { container } = render( + + ); + const regexConditions = container.getElementsByClassName( + "password-criteria-icon" + ); + expect(regexConditions[0]).toHaveClass("fails"); + }); + + test("validates password doesn't have uppercase", () => { + const { container } = render(); + const regexConditions = container.getElementsByClassName( + "password-criteria-icon" + ); + expect(regexConditions[1]).toHaveClass("fails"); + }); + + test("validates password doesn't have lowercase", () => { + const { container } = render(); + const regexConditions = container.getElementsByClassName( + "password-criteria-icon" + ); + expect(regexConditions[2]).toHaveClass("fails"); + }); + + test("validates password doesn't have number", () => { + const { container } = render(); + const regexConditions = container.getElementsByClassName( + "password-criteria-icon" + ); + expect(regexConditions[3]).toHaveClass("fails"); + }); + + test("validates password doesn't have symbol", () => { + const { container } = render( + + ); + const regexConditions = container.getElementsByClassName( + "password-criteria-icon" + ); + expect(regexConditions[4]).toHaveClass("fails"); + }); +}); diff --git a/src/ui/components/PasswordValidation/PasswordValidation.tsx b/src/ui/components/PasswordValidation/PasswordValidation.tsx new file mode 100644 index 000000000..b673a3567 --- /dev/null +++ b/src/ui/components/PasswordValidation/PasswordValidation.tsx @@ -0,0 +1,75 @@ +import { IonList, IonItem, IonIcon, IonLabel } from "@ionic/react"; +import { checkmarkOutline, closeOutline } from "ionicons/icons"; +import { passwordStrengthChecker } from "../../utils/passwordStrengthChecker"; +import { i18n } from "../../../i18n"; +import { ErrorMessage } from "../ErrorMessage"; +import "./PasswordValidation.scss"; + +const PasswordValidation = ({ password }: { password: string }) => { + const ValidationItem = ({ + condition, + label, + }: { + condition: boolean; + label: string; + }) => { + return ( + + + {label} + + ); + }; + + return ( + + {(!passwordStrengthChecker.validatePassword(password) || + !passwordStrengthChecker.isValidCharacters(password)) && ( + + )} + {[ + { + condition: passwordStrengthChecker.isLengthValid(password), + label: i18n.t("operationspasswordregex.label.length"), + }, + { + condition: passwordStrengthChecker.isUppercaseValid(password), + label: i18n.t("operationspasswordregex.label.uppercase"), + }, + { + condition: passwordStrengthChecker.isLowercaseValid(password), + label: i18n.t("operationspasswordregex.label.lowercase"), + }, + { + condition: passwordStrengthChecker.isNumberValid(password), + label: i18n.t("operationspasswordregex.label.number"), + }, + { + condition: passwordStrengthChecker.isSymbolValid(password), + label: i18n.t("operationspasswordregex.label.symbol"), + }, + ].map(({ condition, label }) => ( + + ))} + + ); +}; + +export { PasswordValidation }; diff --git a/src/ui/components/PasswordValidation/index.ts b/src/ui/components/PasswordValidation/index.ts new file mode 100644 index 000000000..33a827c72 --- /dev/null +++ b/src/ui/components/PasswordValidation/index.ts @@ -0,0 +1 @@ +export * from "./PasswordValidation"; diff --git a/src/ui/components/VerifyPassword/VerifyPassword.scss b/src/ui/components/VerifyPassword/VerifyPassword.scss index aca41a6e4..c8c2340ba 100644 --- a/src/ui/components/VerifyPassword/VerifyPassword.scss +++ b/src/ui/components/VerifyPassword/VerifyPassword.scss @@ -3,7 +3,7 @@ display: none; } - ion-item.input-item .input-line { + ion-item.custom-input .input-line { border: none; text-align: center; font-size: 2rem; diff --git a/src/ui/pages/CreatePassword/CreatePassword.scss b/src/ui/pages/CreatePassword/CreatePassword.scss index 30361dcc0..c7100dcd0 100644 --- a/src/ui/pages/CreatePassword/CreatePassword.scss +++ b/src/ui/pages/CreatePassword/CreatePassword.scss @@ -1,46 +1,19 @@ .create-password { - padding-inline: 20px; + --background: var(--ion-color-light); - ion-title { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; + ion-item.error { + margin-bottom: 0; } - h2 { - margin: 0 auto; + .error-message-placeholder { + display: none; } - .operations-password-regex { - padding-bottom: 0; - background-color: var(--ion-color-light); - - ion-item { - --background: var(--ion-color-light); - .password-criteria-icon { - font-size: 1.1rem; - padding: 0.2rem; - margin-right: 0.5rem; - border-radius: 1.5rem; - &.fails { - color: var(--ion-color-secondary); - background: var(--ion-color-light-grey); - } - &.pass { - color: white; - background: var(--ion-color-green); - } - } - - &::part(native) { - padding: 0; - } - } + .error-message { + text-align: left; } - .create-hint { - margin-bottom: 4rem; + .page-footer { + margin-top: 3rem; } } diff --git a/src/ui/pages/CreatePassword/CreatePassword.test.tsx b/src/ui/pages/CreatePassword/CreatePassword.test.tsx index 280d83000..f5804c602 100644 --- a/src/ui/pages/CreatePassword/CreatePassword.test.tsx +++ b/src/ui/pages/CreatePassword/CreatePassword.test.tsx @@ -1,13 +1,7 @@ import { Provider } from "react-redux"; import { render } from "@testing-library/react"; import configureStore from "redux-mock-store"; -import userEvent from "@testing-library/user-event"; -import { - CreatePassword, - PasswordRegex, - PasswordValidator, -} from "./CreatePassword"; -import EN_TRANSLATIONS from "../../../locales/en/en.json"; +import { CreatePassword } from "./CreatePassword"; describe("Create Password Page", () => { const mockStore = configureStore(); @@ -31,112 +25,4 @@ describe("Create Password Page", () => { expect(confirmPasswordValue).toBeInTheDocument(); expect(createHintValue).toBeInTheDocument(); }); - - test("validates password correctly", () => { - const { container } = render(); - const regexConditions = container.getElementsByClassName( - "password-criteria-icon" - ); - for (let i = 0; i < regexConditions.length; i++) { - expect(regexConditions[i]).toHaveClass("pass"); - } - }); - - test("validates password length correctly", () => { - const { container } = render(); - const regexConditions = container.getElementsByClassName( - "password-criteria-icon" - ); - expect(regexConditions[0]).toHaveClass("pass"); - }); - - test("validates password is too short", () => { - const { container } = render(); - const regexConditions = container.getElementsByClassName( - "password-criteria-icon" - ); - expect(regexConditions[0]).toHaveClass("fails"); - }); - - test("validates password is too long", () => { - const { container } = render( - - ); - const regexConditions = container.getElementsByClassName( - "password-criteria-icon" - ); - expect(regexConditions[0]).toHaveClass("fails"); - }); - - test("validates password doesn't have uppercase", () => { - const { container } = render(); - const regexConditions = container.getElementsByClassName( - "password-criteria-icon" - ); - expect(regexConditions[1]).toHaveClass("fails"); - }); - - test("validates password doesn't have lowercase", () => { - const { container } = render(); - const regexConditions = container.getElementsByClassName( - "password-criteria-icon" - ); - expect(regexConditions[2]).toHaveClass("fails"); - }); - - test("validates password doesn't have number", () => { - const { container } = render(); - const regexConditions = container.getElementsByClassName( - "password-criteria-icon" - ); - expect(regexConditions[3]).toHaveClass("fails"); - }); - - test("validates password doesn't have symbol", () => { - const { container } = render(); - const regexConditions = container.getElementsByClassName( - "password-criteria-icon" - ); - expect(regexConditions[4]).toHaveClass("fails"); - }); - - test("PasswordValidator returns false when using special char", () => { - expect(PasswordValidator.isValidCharacters("Abc123! @")).toBe(false); - }); - - test("PasswordValidator returns correct error priority", () => { - expect(PasswordValidator.getErrorByPriority("ABCD")).toBe( - EN_TRANSLATIONS.createpassword.error.isTooShort - ); - expect( - PasswordValidator.getErrorByPriority( - "123456789123456789123456789123456789" - ) - ).toBe(EN_TRANSLATIONS.createpassword.error.isTooLong); - expect(PasswordValidator.getErrorByPriority("ABCD1234@")).toBe( - EN_TRANSLATIONS.createpassword.error.hasNoLowercase - ); - expect(PasswordValidator.getErrorByPriority("abcd1234@")).toBe( - EN_TRANSLATIONS.createpassword.error.hasNoUppercase - ); - expect(PasswordValidator.getErrorByPriority("abcdEFGH@")).toBe( - EN_TRANSLATIONS.createpassword.error.hasNoNumber - ); - expect(PasswordValidator.getErrorByPriority("abcdEFGH12 @")).toBe( - EN_TRANSLATIONS.createpassword.error.hasSpecialChar - ); - }); - - test.skip("show error message on type special char", () => { - const { getByTestId } = render( - - - - ); - - const input = getByTestId("createPasswordValue"); - userEvent.type(input, "Abc123! @"); - const errorMessage = getByTestId("error-message"); - expect(errorMessage).toBeInTheDocument(); - }); }); diff --git a/src/ui/pages/CreatePassword/CreatePassword.tsx b/src/ui/pages/CreatePassword/CreatePassword.tsx index 30001370b..baa9e5b29 100644 --- a/src/ui/pages/CreatePassword/CreatePassword.tsx +++ b/src/ui/pages/CreatePassword/CreatePassword.tsx @@ -1,23 +1,10 @@ import { useState } from "react"; -import { - IonCol, - IonGrid, - IonPage, - IonRow, - IonIcon, - IonItem, - IonLabel, - IonList, -} from "@ionic/react"; -import { closeOutline, checkmarkOutline } from "ionicons/icons"; import { useHistory } from "react-router-dom"; import { i18n } from "../../../i18n"; -import { PageLayout } from "../../components/layout/PageLayout"; import "./CreatePassword.scss"; import { CustomInput } from "../../components/CustomInput"; import { ErrorMessage } from "../../components/ErrorMessage"; import { RoutePath } from "../../../routes"; -import { PasswordRegexProps, RegexItemProps } from "./CreatePassword.types"; import { AriesAgent } from "../../../core/agent/agent"; import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; import { useAppDispatch, useAppSelector } from "../../../store/hooks"; @@ -30,142 +17,35 @@ import { updateReduxState } from "../../../store/utils"; import { Alert } from "../../components/Alert"; import { OperationType } from "../../globals/types"; import { MiscRecordId } from "../../../core/agent/agent.types"; - -const errorMessages = { - hasSpecialChar: i18n.t("createpassword.error.hasSpecialChar"), - isTooShort: i18n.t("createpassword.error.isTooShort"), - isTooLong: i18n.t("createpassword.error.isTooLong"), - hasNoUppercase: i18n.t("createpassword.error.hasNoUppercase"), - hasNoLowercase: i18n.t("createpassword.error.hasNoLowercase"), - hasNoNumber: i18n.t("createpassword.error.hasNoNumber"), - hasNoSymbol: i18n.t("createpassword.error.hasNoSymbol"), - hasNoMatch: i18n.t("createpassword.error.hasNoMatch"), - hintSameAsPassword: i18n.t("createpassword.error.hintSameAsPassword"), -}; - -const PasswordValidator = { - uppercaseRegex: /^(?=.*[A-Z])/, - lowercaseRegex: /^(?=.*[a-z])/, - numberRegex: /^(?=.*[0-9])/, - symbolRegex: /^(?=.*[!@#$%^&*()])/, - validCharactersRegex: /^[a-zA-Z0-9!@#$%^&*()]+$/, - lengthRegex: /^.{8,64}$/, - isLengthValid(password: string) { - return this.lengthRegex.test(password); - }, - isUppercaseValid(password: string) { - return this.uppercaseRegex.test(password); - }, - isLowercaseValid(password: string) { - return this.lowercaseRegex.test(password); - }, - isNumberValid(password: string) { - return this.numberRegex.test(password); - }, - isSymbolValid(password: string) { - return this.symbolRegex.test(password); - }, - isValidCharacters(password: string) { - return this.validCharactersRegex.test(password); - }, - validatePassword(password: string) { - return ( - this.isUppercaseValid(password) && - this.isLowercaseValid(password) && - this.isNumberValid(password) && - this.isSymbolValid(password) && - this.isValidCharacters(password) && - this.isLengthValid(password) - ); - }, - getErrorByPriority(password: string) { - let errorMessage = undefined; - if (password.length < 8) { - errorMessage = errorMessages.isTooShort; - } else if (password.length > 32) { - errorMessage = errorMessages.isTooLong; - } else if (!this.isUppercaseValid(password)) { - errorMessage = errorMessages.hasNoUppercase; - } else if (!this.isLowercaseValid(password)) { - errorMessage = errorMessages.hasNoLowercase; - } else if (!this.isNumberValid(password)) { - errorMessage = errorMessages.hasNoNumber; - } else if (!this.isSymbolValid(password)) { - errorMessage = errorMessages.hasNoSymbol; - } else if (!this.isValidCharacters(password)) { - errorMessage = errorMessages.hasSpecialChar; - } - - return errorMessage; - }, -}; - -const RegexItem = ({ condition, label }: RegexItemProps) => { - return ( - - - {label} - - ); -}; - -const PasswordRegex = ({ password }: PasswordRegexProps) => { - return ( - - - - - - - - ); -}; +import { PageHeader } from "../../components/PageHeader"; +import { ScrollablePageLayout } from "../../components/layout/ScrollablePageLayout"; +import PageFooter from "../../components/PageFooter/PageFooter"; +import { passwordStrengthChecker } from "../../utils/passwordStrengthChecker"; +import { PasswordValidation } from "../../components/PasswordValidation"; const CreatePassword = () => { + const pageId = "create-password"; const stateCache = useAppSelector(getStateCache); const history = useHistory(); const dispatch = useAppDispatch(); const [createPasswordValue, setCreatePasswordValue] = useState(""); const [confirmPasswordValue, setConfirmPasswordValue] = useState(""); const [confirmPasswordFocus, setConfirmPasswordFocus] = useState(false); - const [passwordFocus, setPasswordFocus] = useState(false); - const [createHintValue, setCreateHintValue] = useState(""); + const [createPasswordFocus, setCreatePasswordFocus] = useState(false); + const [hintValue, setHintValue] = useState(""); const [alertIsOpen, setAlertIsOpen] = useState(false); - - const passwordValueMatching = + const createPasswordValueMatching = createPasswordValue.length > 0 && confirmPasswordValue.length > 0 && createPasswordValue === confirmPasswordValue; - const passwordValueNotMatching = + const createPasswordValueNotMatching = createPasswordValue.length > 0 && confirmPasswordValue.length > 0 && createPasswordValue !== confirmPasswordValue; const validated = - PasswordValidator.validatePassword(createPasswordValue) && - passwordValueMatching && - createHintValue !== createPasswordValue; + passwordStrengthChecker.validatePassword(createPasswordValue) && + createPasswordValueMatching && + hintValue !== createPasswordValue; const handlePasswordInput = (password: string) => { setCreatePasswordValue(password); @@ -173,11 +53,7 @@ const CreatePassword = () => { const handleClearState = () => { setCreatePasswordValue(""); setConfirmPasswordValue(""); - setCreateHintValue(""); - }; - const handleClose = async () => { - handleClearState(); - handleContinue(true); + setHintValue(""); }; const handleContinue = async (skipped: boolean) => { @@ -187,28 +63,24 @@ const CreatePassword = () => { KeyStoreKeys.APP_OP_PASSWORD, createPasswordValue ); - if (createHintValue) { + if (hintValue) { await AriesAgent.agent.genericRecords.save({ id: MiscRecordId.OP_PASS_HINT, - content: { value: createHintValue }, + content: { value: hintValue }, }); } } const { nextPath, updateRedux } = getNextRoute(RoutePath.CREATE_PASSWORD, { store: { stateCache }, - state: { - skipped, - }, + state: { skipped }, }); updateReduxState( nextPath.pathname, { store: { stateCache }, - state: { - skipped, - }, + state: { skipped }, }, dispatch, updateRedux @@ -219,125 +91,94 @@ const CreatePassword = () => { }; return ( - - handleClose()} - title={`${i18n.t("createpassword.title")}`} - footer={true} + + } + > +

{i18n.t("createpassword.title")}

+

{i18n.t("createpassword.description")}

+ handlePasswordInput(password)} + onChangeFocus={setCreatePasswordFocus} + value={createPasswordValue} + error={ + !createPasswordFocus && + !!createPasswordValue.length && + (!passwordStrengthChecker.validatePassword(createPasswordValue) || + !passwordStrengthChecker.isValidCharacters(createPasswordValue)) + } + /> + {createPasswordValue && ( + + )} + + {!confirmPasswordFocus && + !!confirmPasswordValue.length && + createPasswordValueNotMatching && ( + + )} + + {!!hintValue.length && hintValue === createPasswordValue && ( + + )} + + handleContinue(false)} primaryButtonDisabled={!validated} - secondaryButtonText={`${i18n.t("createpassword.button.skip")}`} - secondaryButtonAction={() => setAlertIsOpen(true)} - > - - - -

- {i18n.t("createpassword.description")} -

-
-
-
- - - - - handlePasswordInput(password) - } - onChangeFocus={setPasswordFocus} - value={createPasswordValue} - /> - - - {(createPasswordValue !== "" && - !PasswordValidator.validatePassword(createPasswordValue)) || - !PasswordValidator.isValidCharacters(createPasswordValue) ? ( - - ) : null} - {createPasswordValue && ( - - - - - - )} - - - - - - - - {confirmPasswordFocus && passwordValueNotMatching ? ( - - ) : null} - - - - - - - - {createHintValue && createHintValue === createPasswordValue ? ( - - ) : null} - - handleContinue(true)} - /> -
-
+ tertiaryButtonText={`${i18n.t("createpassword.button.skip")}`} + tertiaryButtonAction={() => setAlertIsOpen(true)} + /> + handleContinue(true)} + /> + ); }; -export { CreatePassword, PasswordRegex, PasswordValidator }; +export { CreatePassword }; diff --git a/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.tsx b/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.tsx index 0aa5f058c..6036b9559 100644 --- a/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.tsx +++ b/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.tsx @@ -123,7 +123,7 @@ const GenerateSeedPhrase = () => { beforeBack={handleClearState} currentPath={RoutePath.GENERATE_SEED_PHRASE} progressBar={true} - progressBarValue={0.66} + progressBarValue={0.75} progressBarBuffer={1} /> } diff --git a/src/ui/pages/Onboarding/Onboarding.test.tsx b/src/ui/pages/Onboarding/Onboarding.test.tsx index ef3c9ce3f..a68fd2ebe 100644 --- a/src/ui/pages/Onboarding/Onboarding.test.tsx +++ b/src/ui/pages/Onboarding/Onboarding.test.tsx @@ -10,6 +10,7 @@ import { store } from "../../../store"; import { RoutePath } from "../../../routes"; import { FIFTEEN_WORDS_BIT_LENGTH } from "../../globals/constants"; import { OperationType } from "../../globals/types"; +import { CreatePassword } from "../CreatePassword"; describe("Onboarding Page", () => { test("Render slide 1", () => { @@ -94,8 +95,8 @@ describe("Onboarding Page", () => { component={Onboarding} /> @@ -108,9 +109,7 @@ describe("Onboarding Page", () => { fireEvent.click(buttonContinue); await waitFor(() => { - expect( - queryByText(EN_TRANSLATIONS.generateseedphrase.onboarding.title) - ).toBeVisible(); + expect(queryByText(EN_TRANSLATIONS.createpassword.title)).toBeVisible(); }); }); }); diff --git a/src/ui/pages/SetPasscode/SetPasscode.tsx b/src/ui/pages/SetPasscode/SetPasscode.tsx index 37ab2b856..ca819fe6d 100644 --- a/src/ui/pages/SetPasscode/SetPasscode.tsx +++ b/src/ui/pages/SetPasscode/SetPasscode.tsx @@ -90,14 +90,14 @@ const SetPasscode = () => { beforeBack={handleBeforeBack} currentPath={RoutePath.SET_PASSCODE} progressBar={true} - progressBarValue={0.33} + progressBarValue={0.25} progressBarBuffer={1} /> } >

{originalPassCode !== "" ? i18n.t("setpasscode.reenterpasscode.title") diff --git a/src/ui/utils/passwordStrengthChecker.test.ts b/src/ui/utils/passwordStrengthChecker.test.ts new file mode 100644 index 000000000..982773bf4 --- /dev/null +++ b/src/ui/utils/passwordStrengthChecker.test.ts @@ -0,0 +1,33 @@ +import { passwordStrengthChecker } from "./passwordStrengthChecker"; + +describe("passwordStrengthChecker", () => { + it("should return true if the password contains an uppercase letter", () => { + const password = "Cardano1$"; + expect(passwordStrengthChecker.isUppercaseValid(password)).toBe(true); + }); + it("should return true if the password contains a lower case letter", () => { + const password = "Cardano1$"; + expect(passwordStrengthChecker.isLowercaseValid(password)).toBe(true); + }); + it("should return true if the password contains a number", () => { + const password = "Cardano1$"; + expect(passwordStrengthChecker.isNumberValid(password)).toBe(true); + }); + it("should return true if the password contains a symbol", () => { + const password = "Cardano1$"; + expect(passwordStrengthChecker.isSymbolValid(password)).toBe(true); + }); + it("should return true if password contains valid characters", () => { + const password = "Cardano1$"; + expect(passwordStrengthChecker.isValidCharacters(password)).toBe(true); + }); + it("should return true if password length is less than 64", () => { + const password = "Cardano1$"; + expect(passwordStrengthChecker.isLengthValid(password)).toBe(true); + }); + it("should return false if password length is 64 or more", () => { + const password = + "Abc123456789012345678901234567890123456789012345678901234567890123@"; + expect(passwordStrengthChecker.isLengthValid(password)).toBe(false); + }); +}); diff --git a/src/ui/utils/passwordStrengthChecker.ts b/src/ui/utils/passwordStrengthChecker.ts new file mode 100644 index 000000000..5454a2403 --- /dev/null +++ b/src/ui/utils/passwordStrengthChecker.ts @@ -0,0 +1,72 @@ +import { i18n } from "../../i18n"; + +const passwordRequirements = { + uppercasePattern: /^(?=.*[A-Z])/, + lowercasePattern: /^(?=.*[a-z])/, + numberPattern: /^(?=.*[0-9])/, + symbolPattern: /^(?=.*[!@#$%^&*()])/, + validCharactersPattern: /^[a-zA-Z0-9!@#$%^&*()]+$/, + lengthPattern: /^.{8,64}$/, +}; + +const errorMessages = { + hasSpecialChar: i18n.t("createpassword.error.hasSpecialChar"), + isTooShort: i18n.t("createpassword.error.isTooShort"), + isTooLong: i18n.t("createpassword.error.isTooLong"), + hasNoUppercase: i18n.t("createpassword.error.hasNoUppercase"), + hasNoLowercase: i18n.t("createpassword.error.hasNoLowercase"), + hasNoNumber: i18n.t("createpassword.error.hasNoNumber"), + hasNoSymbol: i18n.t("createpassword.error.hasNoSymbol"), +}; + +const passwordStrengthChecker = { + isLengthValid(password: string): boolean { + return passwordRequirements.lengthPattern.test(password); + }, + isUppercaseValid(password: string): boolean { + return passwordRequirements.uppercasePattern.test(password); + }, + isLowercaseValid(password: string): boolean { + return passwordRequirements.lowercasePattern.test(password); + }, + isNumberValid(password: string): boolean { + return passwordRequirements.numberPattern.test(password); + }, + isSymbolValid(password: string): boolean { + return passwordRequirements.symbolPattern.test(password); + }, + isValidCharacters(password: string): boolean { + return passwordRequirements.validCharactersPattern.test(password); + }, + validatePassword(password: string): boolean { + return ( + this.isUppercaseValid(password) && + this.isLowercaseValid(password) && + this.isNumberValid(password) && + this.isSymbolValid(password) && + this.isValidCharacters(password) && + this.isLengthValid(password) + ); + }, + getErrorByPriority(password: string): string | undefined { + if (password.length < 8) { + return errorMessages.isTooShort; + } else if (password.length > 32) { + return errorMessages.isTooLong; + } else if (!this.isUppercaseValid(password)) { + return errorMessages.hasNoUppercase; + } else if (!this.isLowercaseValid(password)) { + return errorMessages.hasNoLowercase; + } else if (!this.isNumberValid(password)) { + return errorMessages.hasNoNumber; + } else if (!this.isSymbolValid(password)) { + return errorMessages.hasNoSymbol; + } else if (!this.isValidCharacters(password)) { + return errorMessages.hasSpecialChar; + } + + return undefined; + }, +}; + +export { passwordStrengthChecker };