diff --git a/src/locales/en/en.json b/src/locales/en/en.json index 659d2fa7d..a189d52fa 100644 --- a/src/locales/en/en.json +++ b/src/locales/en/en.json @@ -48,20 +48,65 @@ }, "generateseedphrase": { "onboarding": { - "title": "Generate recovery phrase", + "title": "Your recovery phrase", "paragraph": { - "top": "Think of your secret recovery phrase as a safety net for your identity. If you ever lose your phone or switch to a new wallet, this phrase will help you recover your identity.", - "bottom": "It's important to keep these words safe and sound! Store them in a secure location and remember to never share them with anyone." + "bottom": "Keep these words safe! Store them securely and never share them with anyone." }, "button": { - "continue": "Generate", - "switch": "Switch to recover a wallet" + "continue": "Continue", + "switch": "Switch to recover a wallet", + "recoverydocumentation": "What’s a recovery phrase?" + }, + "recoveryseedphrasedocs": { + "title": "Recovery phrase", + "button": { + "done": "Done" + }, + "alert": "We can’t recover your wallet if you lose your recovery phrase. Keeping it safe means keeping your identity safe.", + "content": { + "what": { + "title": "What is a recovery phrase?", + "content": [ + { + "text": "Your recovery phrase is like a master key for your wallet. It’s a unique set of 18 words that lets you securely access your identity wallet—even if you lose your phone or need to restore it on a new device." + } + ] + }, + "why": { + "title": "Why is it so important?", + "content": [ + { + "text": "Your wallet, your control: Unlike traditional accounts, no one—not even us—can reset your wallet for you. This keeps your identity and data secure and fully under your control." + }, + { + "text": "One-of-a-kind key: The recovery phrase is the only way to restore your wallet if something happens to your device. Without it, you could permanently lose access to your identity." + }, + { + "text": "Stay protected: Think of it like a spare key to your house. If you lose it, no one can unlock the door—even you!" + } + ] + }, + "how": { + "title": "How to keep it safe:", + "content": [ + { + "text": "Write it down: Store it somewhere secure, like a locked drawer or safe." + }, + { + "text": "Don’t share it: Anyone with your recovery phrase can access your wallet. Keep it private." + }, + { + "text": "No screenshots: Don’t save it on your phone or in the cloud—this protects it from hackers." + } + ] + } + } } }, "segment": "{{length}} words", "privacy": { "overlay": { - "text": "Press the ‘view’ button when you’re ready to see your recovery phrase. Remember to make sure nobody is looking!", + "text": "Press the ‘view recovery phrase’ button when you’re ready to see your recovery phrase. Remember to make sure nobody is looking!", "button": "View Recovery Phrase" } }, @@ -1819,4 +1864,4 @@ "more": "Read more", "less": "Read less" } -} +} \ No newline at end of file diff --git a/src/ui/App.scss b/src/ui/App.scss index 6ccdd39d5..d9945765f 100644 --- a/src/ui/App.scss +++ b/src/ui/App.scss @@ -181,6 +181,10 @@ p { .tertiary-button { --color: var(--ion-color-primary); --background: transparent; + + ion-icon { + margin-right: 0.5rem; + } } .delete-button, diff --git a/src/ui/assets/images/SeedPhraseDocs.png b/src/ui/assets/images/SeedPhraseDocs.png new file mode 100644 index 000000000..67a152ab3 Binary files /dev/null and b/src/ui/assets/images/SeedPhraseDocs.png differ diff --git a/src/ui/components/InfoCard/InfoCard.scss b/src/ui/components/InfoCard/InfoCard.scss index 802d882ac..31728ad61 100644 --- a/src/ui/components/InfoCard/InfoCard.scss +++ b/src/ui/components/InfoCard/InfoCard.scss @@ -6,6 +6,18 @@ color: var(--ion-color-secondary); padding: 1rem 1.25rem; + &.danger { + &.info-card { + background: rgba(var(--ion-color-danger-rgb), 0.1); + + .alert-icon { + ion-icon { + color: var(--ion-color-danger); + } + } + } + } + p { margin: 0; } diff --git a/src/ui/components/InfoCard/InfoCard.tsx b/src/ui/components/InfoCard/InfoCard.tsx index 36414f680..423db233a 100644 --- a/src/ui/components/InfoCard/InfoCard.tsx +++ b/src/ui/components/InfoCard/InfoCard.tsx @@ -4,8 +4,10 @@ import { InfoCardProps } from "./InfoCard.types"; import { combineClassNames } from "../../utils/style"; import "./InfoCard.scss"; -const InfoCard = ({content ,className, icon}: InfoCardProps) => { - const classes = combineClassNames("info-card", className); +const InfoCard = ({content ,className, icon, danger}: InfoCardProps) => { + const classes = combineClassNames("info-card", className, { + "danger": !!danger + }); return ( diff --git a/src/ui/components/InfoCard/InfoCard.types.ts b/src/ui/components/InfoCard/InfoCard.types.ts index 5d7a90c92..a135bbe02 100644 --- a/src/ui/components/InfoCard/InfoCard.types.ts +++ b/src/ui/components/InfoCard/InfoCard.types.ts @@ -2,6 +2,7 @@ interface InfoCardProps { content: string; className?: string; icon?: string; + danger?: boolean; } export type { InfoCardProps }; \ No newline at end of file diff --git a/src/ui/components/RecoverySeedPhraseModule/RecoverySeedPhraseModule.tsx b/src/ui/components/RecoverySeedPhraseModule/RecoverySeedPhraseModule.tsx index b2e0cadcd..525e8ce1a 100644 --- a/src/ui/components/RecoverySeedPhraseModule/RecoverySeedPhraseModule.tsx +++ b/src/ui/components/RecoverySeedPhraseModule/RecoverySeedPhraseModule.tsx @@ -1,6 +1,6 @@ import { IonButton, IonIcon } from "@ionic/react"; import { wordlists } from "bip39"; -import { closeOutline } from "ionicons/icons"; +import { closeOutline, refreshOutline } from "ionicons/icons"; import { forwardRef, useImperativeHandle, useRef, useState } from "react"; import { Agent } from "../../../core/agent/agent"; import { i18n } from "../../../i18n"; @@ -11,8 +11,8 @@ import { Alert as AlertFail } from "../Alert"; import { PageFooter } from "../PageFooter"; import { SeedPhraseModule } from "../SeedPhraseModule"; import { SeedPhraseModuleRef } from "../SeedPhraseModule/SeedPhraseModule.types"; -import { SwitchOnboardingMode } from "../SwitchOnboardingMode"; -import { OnboardingMode } from "../SwitchOnboardingMode/SwitchOnboardingMode.types"; +import { SwitchOnboardingModeModal } from "../SwitchOnboardingModeModal"; +import { OnboardingMode } from "../SwitchOnboardingModeModal/SwitchOnboardingModeModal.types"; import "./RecoverySeedPhraseModule.scss"; import { RecoverySeedPhraseModuleProps, @@ -44,7 +44,7 @@ const RecoverySeedPhraseModule = forwardRef< const [alertIsOpen, setAlertIsOpen] = useState(false); const [clearAlertOpen, setClearAlertOpen] = useState(false); - + const [showSwitchModeModal, setSwitchModeModal] = useState(false); const [seedPhraseInfo, setSeedPhraseInfo] = useState([ { value: "", @@ -289,9 +289,6 @@ const RecoverySeedPhraseModule = forwardRef< {i18n.t("verifyrecoveryseedphrase.button.clear")} )} - {displaySwitchModeButton && ( - - )} setSwitchModeModal(true)} + tertiaryButtonIcon={refreshOutline} /> + ); } diff --git a/src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.tsx b/src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.tsx deleted file mode 100644 index b87adc1b0..000000000 --- a/src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { IonButton, IonCheckbox, IonIcon, IonModal } from "@ionic/react"; -import { swapHorizontalOutline } from "ionicons/icons"; -import { useState } from "react"; -import { Trans } from "react-i18next"; -import { i18n } from "../../../i18n"; -import { - OnboardingMode, - SwitchOnboardingModeProps, -} from "./SwitchOnboardingMode.types"; -import "./SwitchOnboardingMode.scss"; -import { ScrollablePageLayout } from "../layout/ScrollablePageLayout"; -import { PageHeader } from "../PageHeader"; -import { CardDetailsBlock } from "../CardDetails"; -import { PageFooter } from "../PageFooter"; -import { useAppDispatch, useAppSelector } from "../../../store/hooks"; -import { setSeedPhraseCache } from "../../../store/reducers/seedPhraseCache"; -import { Agent } from "../../../core/agent/agent"; -import { BasicRecord } from "../../../core/agent/records"; -import { MiscRecordId } from "../../../core/agent/agent.types"; -import { useAppIonRouter } from "../../hooks"; -import { RoutePath } from "../../../routes"; -import { - getAuthentication, - setAuthentication, - setCurrentRoute, -} from "../../../store/reducers/stateCache"; -import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; -import { showError } from "../../utils/error"; - -const SwitchOnboardingMode = ({ mode }: SwitchOnboardingModeProps) => { - const dispatch = useAppDispatch(); - const authentication = useAppSelector(getAuthentication); - const [openConfirmModal, setOpenConfirmModal] = useState(false); - const [agree, setAgree] = useState(false); - const ionRouter = useAppIonRouter(); - - const isCreateMode = mode === OnboardingMode.Create; - const buttonLabel = !isCreateMode - ? i18n.t("generateseedphrase.onboarding.button.switch") - : i18n.t("verifyrecoveryseedphrase.button.switch"); - - const handleContinue = async () => { - try { - const action = isCreateMode - ? Agent.agent.basicStorage.deleteById(MiscRecordId.APP_RECOVERY_WALLET) - : Agent.agent.basicStorage.createOrUpdateBasicRecord( - new BasicRecord({ - id: MiscRecordId.APP_RECOVERY_WALLET, - content: { - value: String(true), - }, - }) - ); - - await Promise.all([ - SecureStorage.delete(KeyStoreKeys.SIGNIFY_BRAN), - action, - ]); - - dispatch( - setAuthentication({ - ...authentication, - recoveryWalletProgress: !isCreateMode, - }) - ); - - dispatch( - setSeedPhraseCache({ - bran: "", - seedPhrase: "", - }) - ); - - const nextPath = isCreateMode - ? RoutePath.GENERATE_SEED_PHRASE - : RoutePath.VERIFY_RECOVERY_SEED_PHRASE; - - dispatch(setCurrentRoute({ path: nextPath })); - ionRouter.push(nextPath); - } catch (e) { - showError("Unable to switch onboarding mode", e, dispatch); - } - }; - - return ( - <> - setOpenConfirmModal(true)} - fill="outline" - data-testid="switch-mode-button" - className="switch-button secondary-button" - > - - {buttonLabel} - - setOpenConfirmModal(false)} - > - setOpenConfirmModal(false)} - title={`${i18n.t("switchmodemodal.title")}`} - /> - } - footer={ - handleContinue()} - primaryButtonDisabled={!agree} - > -
- setAgree(event.detail.checked)} - /> -

{i18n.t("switchmodemodal.checkbox")}

-
-
- } - > -

{i18n.t(`switchmodemodal.${mode}.title`)}

-

- {i18n.t(`switchmodemodal.${mode}.paragraphtop`)} -

- -
    -
  1. - {i18n.t(`switchmodemodal.${mode}.warning.one`)} -
  2. -
  3. - {i18n.t(`switchmodemodal.${mode}.warning.two`)} -
  4. -
  5. - {i18n.t(`switchmodemodal.${mode}.warning.three`)} -
  6. -
-
-

- {i18n.t(`switchmodemodal.${mode}.paragraphbot`)} -

-
-
- - ); -}; - -export { SwitchOnboardingMode }; diff --git a/src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.types.ts b/src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.types.ts deleted file mode 100644 index 5a5e300a5..000000000 --- a/src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.types.ts +++ /dev/null @@ -1,11 +0,0 @@ -enum OnboardingMode { - Create = "create", - Recovery = "recovery", -} - -interface SwitchOnboardingModeProps { - mode: OnboardingMode; -} - -export type { SwitchOnboardingModeProps }; -export { OnboardingMode }; diff --git a/src/ui/components/SwitchOnboardingMode/index.ts b/src/ui/components/SwitchOnboardingMode/index.ts deleted file mode 100644 index 55fdc0afb..000000000 --- a/src/ui/components/SwitchOnboardingMode/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./SwitchOnboardingMode"; diff --git a/src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.scss b/src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.scss similarity index 100% rename from src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.scss rename to src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.scss diff --git a/src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.test.tsx b/src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.test.tsx similarity index 83% rename from src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.test.tsx rename to src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.test.tsx index a2b777123..093ee8c90 100644 --- a/src/ui/components/SwitchOnboardingMode/SwitchOnboardingMode.test.tsx +++ b/src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.test.tsx @@ -3,8 +3,8 @@ import { act } from "react"; import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; import EN_TRANSLATIONS from "../../../locales/en/en.json"; -import { SwitchOnboardingMode } from "./SwitchOnboardingMode"; -import { OnboardingMode } from "./SwitchOnboardingMode.types"; +import { SwitchOnboardingModeModal } from "./SwitchOnboardingModeModal"; +import { OnboardingMode } from "./SwitchOnboardingModeModal.types"; const deleteById = jest.fn(); const createNew = jest.fn(); @@ -68,20 +68,10 @@ describe("Switch onboarding mode", () => { require("@ionic/react"); const { getByText, getByTestId } = render( - + ); - expect( - getByText(EN_TRANSLATIONS.verifyrecoveryseedphrase.button.switch) - ).toBeInTheDocument(); - - act(() => { - fireEvent.click( - getByText(EN_TRANSLATIONS.verifyrecoveryseedphrase.button.switch) - ); - }); - await waitFor(() => { expect(getByText(EN_TRANSLATIONS.switchmodemodal.title)).toBeVisible(); }); @@ -129,20 +119,10 @@ describe("Switch onboarding mode", () => { require("@ionic/react"); const { getByText, getByTestId } = render( - + ); - expect( - getByText(EN_TRANSLATIONS.generateseedphrase.onboarding.button.switch) - ).toBeInTheDocument(); - - act(() => { - fireEvent.click( - getByText(EN_TRANSLATIONS.generateseedphrase.onboarding.button.switch) - ); - }); - await waitFor(() => { expect(getByText(EN_TRANSLATIONS.switchmodemodal.title)).toBeVisible(); }); diff --git a/src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.tsx b/src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.tsx new file mode 100644 index 000000000..923661143 --- /dev/null +++ b/src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.tsx @@ -0,0 +1,142 @@ +import { IonCheckbox, IonModal } from "@ionic/react"; +import { useState } from "react"; +import { Trans } from "react-i18next"; +import { Agent } from "../../../core/agent/agent"; +import { MiscRecordId } from "../../../core/agent/agent.types"; +import { BasicRecord } from "../../../core/agent/records"; +import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; +import { i18n } from "../../../i18n"; +import { RoutePath } from "../../../routes"; +import { useAppDispatch, useAppSelector } from "../../../store/hooks"; +import { setSeedPhraseCache } from "../../../store/reducers/seedPhraseCache"; +import { + getAuthentication, + setAuthentication, + setCurrentRoute, +} from "../../../store/reducers/stateCache"; +import { useAppIonRouter } from "../../hooks"; +import { showError } from "../../utils/error"; +import { CardDetailsBlock } from "../CardDetails"; +import { ScrollablePageLayout } from "../layout/ScrollablePageLayout"; +import { PageFooter } from "../PageFooter"; +import { PageHeader } from "../PageHeader"; +import "./SwitchOnboardingModeModal.scss"; +import { + OnboardingMode, + SwitchOnboardingModeModalProps, +} from "./SwitchOnboardingModeModal.types"; + +const SwitchOnboardingModeModal = ({ mode, isOpen, setOpen }: SwitchOnboardingModeModalProps) => { + const dispatch = useAppDispatch(); + const authentication = useAppSelector(getAuthentication); + const [agree, setAgree] = useState(false); + const ionRouter = useAppIonRouter(); + + const isCreateMode = mode === OnboardingMode.Create; + + const handleContinue = async () => { + try { + const action = isCreateMode + ? Agent.agent.basicStorage.deleteById(MiscRecordId.APP_RECOVERY_WALLET) + : Agent.agent.basicStorage.createOrUpdateBasicRecord( + new BasicRecord({ + id: MiscRecordId.APP_RECOVERY_WALLET, + content: { + value: String(true), + }, + }) + ); + + await Promise.all([ + SecureStorage.delete(KeyStoreKeys.SIGNIFY_BRAN), + action, + ]); + + dispatch( + setAuthentication({ + ...authentication, + recoveryWalletProgress: !isCreateMode, + }) + ); + + dispatch( + setSeedPhraseCache({ + bran: "", + seedPhrase: "", + }) + ); + + const nextPath = isCreateMode + ? RoutePath.GENERATE_SEED_PHRASE + : RoutePath.VERIFY_RECOVERY_SEED_PHRASE; + + dispatch(setCurrentRoute({ path: nextPath })); + ionRouter.push(nextPath); + } catch (e) { + showError("Unable to switch onboarding mode", e, dispatch); + } + }; + + return ( + setOpen(false)} + > + setOpen(false)} + title={`${i18n.t("switchmodemodal.title")}`} + /> + } + footer={ + handleContinue()} + primaryButtonDisabled={!agree} + > +
+ setAgree(event.detail.checked)} + /> +

{i18n.t("switchmodemodal.checkbox")}

+
+
+ } + > +

{i18n.t(`switchmodemodal.${mode}.title`)}

+

+ {i18n.t(`switchmodemodal.${mode}.paragraphtop`)} +

+ +
    +
  1. + {i18n.t(`switchmodemodal.${mode}.warning.one`)} +
  2. +
  3. + {i18n.t(`switchmodemodal.${mode}.warning.two`)} +
  4. +
  5. + {i18n.t(`switchmodemodal.${mode}.warning.three`)} +
  6. +
+
+

+ {i18n.t(`switchmodemodal.${mode}.paragraphbot`)} +

+
+
+ ); +}; + +export { SwitchOnboardingModeModal }; diff --git a/src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.types.ts b/src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.types.ts new file mode 100644 index 000000000..114753df3 --- /dev/null +++ b/src/ui/components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.types.ts @@ -0,0 +1,13 @@ +enum OnboardingMode { + Create = "create", + Recovery = "recovery", +} + +interface SwitchOnboardingModeModalProps { + mode: OnboardingMode; + isOpen: boolean; + setOpen: (value: boolean) => void; +} + +export type { SwitchOnboardingModeModalProps }; +export { OnboardingMode }; diff --git a/src/ui/components/SwitchOnboardingModeModal/index.ts b/src/ui/components/SwitchOnboardingModeModal/index.ts new file mode 100644 index 000000000..5fce39620 --- /dev/null +++ b/src/ui/components/SwitchOnboardingModeModal/index.ts @@ -0,0 +1 @@ +export * from "./SwitchOnboardingModeModal"; diff --git a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.scss b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.scss index f4b8a87d1..273de65d4 100644 --- a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.scss +++ b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.scss @@ -14,7 +14,7 @@ display: flex; flex-direction: column; justify-content: space-between; - min-height: calc(100vh - (5.25rem + var(--ion-safe-area-top))); + min-height: calc(100vh - (4rem + var(--ion-safe-area-top))); & .title { margin-top: 1rem; diff --git a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx index ef13c81f4..e43d8517a 100644 --- a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx +++ b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx @@ -451,6 +451,21 @@ describe("SSI agent page", () => { }); }); }); + test("Show switch onboarding modal", async () => { + const { getByText, getByTestId } = render( + + + + ); + + expect(getByText(EN_TRANSLATIONS.generateseedphrase.onboarding.button.switch)).toBeVisible(); + + fireEvent.click(getByTestId("tertiary-button-create-ssi-agent")); + + await waitFor(() => { + expect(getByText(EN_TRANSLATIONS.switchmodemodal.title)).toBeVisible(); + }) + }); }); describe("SSI agent page: recovery mode", () => { diff --git a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx index 9dc477d02..98fde2d3f 100644 --- a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx +++ b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx @@ -1,8 +1,10 @@ +import { Browser } from "@capacitor/browser"; import { IonButton, IonIcon, IonSpinner } from "@ionic/react"; import { informationCircleOutline, - scanOutline, openOutline, + scanOutline, + refreshOutline } from "ionicons/icons"; import { MouseEvent as ReactMouseEvent, @@ -10,7 +12,6 @@ import { useMemo, useState, } from "react"; -import { Browser } from "@capacitor/browser"; import { Agent } from "../../../core/agent/agent"; import { MiscRecordId } from "../../../core/agent/agent.types"; import { ConfigurationService } from "../../../core/configuration"; @@ -34,19 +35,19 @@ import { CustomInput } from "../../components/CustomInput"; import { ErrorMessage } from "../../components/ErrorMessage"; import { PageFooter } from "../../components/PageFooter"; import { PageHeader } from "../../components/PageHeader"; +import { SwitchOnboardingModeModal } from "../../components/SwitchOnboardingModeModal"; import { TermsModal } from "../../components/TermsModal"; import { ScrollablePageLayout } from "../../components/layout/ScrollablePageLayout"; -import { OperationType, ToastMsgType } from "../../globals/types"; -import { useAppIonRouter } from "../../hooks"; -import { isValidHttpUrl } from "../../utils/urlChecker"; -import "./CreateSSIAgent.scss"; -import { SwitchOnboardingMode } from "../../components/SwitchOnboardingMode"; -import { OnboardingMode } from "../../components/SwitchOnboardingMode/SwitchOnboardingMode.types"; import { - RECOVERY_DOCUMENTATION_LINK, ONBOARDING_DOCUMENTATION_LINK, + RECOVERY_DOCUMENTATION_LINK, } from "../../globals/constants"; +import { OperationType, ToastMsgType } from "../../globals/types"; +import { useAppIonRouter } from "../../hooks"; import { showError } from "../../utils/error"; +import { isValidHttpUrl } from "../../utils/urlChecker"; +import "./CreateSSIAgent.scss"; +import { OnboardingMode } from "../../components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.types"; const SSI_URLS_EMPTY = "SSI url is empty"; const SEED_PHRASE_EMPTY = "Invalid seed phrase"; @@ -80,6 +81,7 @@ const CreateSSIAgent = () => { const [hasMismatchError, setHasMismatchError] = useState(false); const [isInvalidBootUrl, setIsInvalidBootUrl] = useState(false); const [isInvalidConnectUrl, setInvalidConnectUrl] = useState(false); + const [showSwitchModeModal, setSwitchModeModal] = useState(false); const isRecoveryMode = stateCache.authentication.recoveryWalletProgress; @@ -279,6 +281,14 @@ const CreateSSIAgent = () => { : ONBOARDING_DOCUMENTATION_LINK, }); }; + + + const mode = isRecoveryMode ? OnboardingMode.Create : OnboardingMode.Recovery; + + const buttonLabel = !isRecoveryMode + ? i18n.t("generateseedphrase.onboarding.button.switch") + : i18n.t("verifyrecoveryseedphrase.button.switch"); + return ( <> { : `${i18n.t("ssiagent.error.invalidconnecturl")}` } /> - handleValidate()} primaryButtonDisabled={!validated || loading} + tertiaryButtonText={buttonLabel} + tertiaryButtonAction={() => setSwitchModeModal(true)} + tertiaryButtonIcon={refreshOutline} /> @@ -436,6 +444,7 @@ const CreateSSIAgent = () => { : `${i18n.t("ssiagent.button.onboardingdocumentation")}`} + ); }; diff --git a/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.test.tsx b/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.test.tsx index 20835d7ee..0c6895f65 100644 --- a/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.test.tsx +++ b/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.test.tsx @@ -228,4 +228,70 @@ describe("Generate Seed Phrase screen from Onboarding", () => { expect(seedNumberElements.length).toBe(18); }); }); + + test("Show switch onboarding modal", async () => { + const initialState = { + stateCache: { + routes: [RoutePath.GENERATE_SEED_PHRASE], + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + }, + currentOperation: OperationType.IDLE, + }, + seedPhraseCache: { + seedPhrase: "", + bran: "", + }, + }; + + const { getByTestId, getByText } = render( + + + + + + ); + + expect(getByText(EN_TRANSLATIONS.generateseedphrase.onboarding.button.switch)).toBeVisible(); + + fireEvent.click(getByTestId("tertiary-button-generate-seed-phrase")); + + await waitFor(() => { + expect(getByText(EN_TRANSLATIONS.switchmodemodal.title)).toBeVisible(); + }) + }); + + test("Show recovery docs", async () => { + const initialState = { + stateCache: { + routes: [RoutePath.GENERATE_SEED_PHRASE], + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + }, + currentOperation: OperationType.IDLE, + }, + seedPhraseCache: { + seedPhrase: "", + bran: "", + }, + }; + + const { getByTestId, getByText } = render( + + + + + + ); + + fireEvent.click(getByTestId("recovery-phrase-docs-btn")); + + await waitFor(() => { + expect(getByText(EN_TRANSLATIONS.generateseedphrase.onboarding.recoveryseedphrasedocs.title)).toBeVisible(); + }) + }); }); diff --git a/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.tsx b/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.tsx index 292a62d48..42ee8e6d1 100644 --- a/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.tsx +++ b/src/ui/pages/GenerateSeedPhrase/GenerateSeedPhrase.tsx @@ -1,28 +1,30 @@ +import { IonButton, IonCheckbox, IonIcon } from "@ionic/react"; +import { informationCircleOutline, refreshOutline } from "ionicons/icons"; import { useEffect, useState } from "react"; -import { IonCheckbox } from "@ionic/react"; -import { useHistory } from "react-router-dom"; -import "./GenerateSeedPhrase.scss"; import { Trans } from "react-i18next"; +import { useHistory } from "react-router-dom"; +import { Agent } from "../../../core/agent/agent"; +import { BranAndMnemonic } from "../../../core/agent/agent.types"; import { i18n } from "../../../i18n"; -import { Alert as AlertConfirm } from "../../components/Alert"; -import { getStateCache } from "../../../store/reducers/stateCache"; -import { getNextRoute } from "../../../routes/nextRoute"; -import { useAppDispatch, useAppSelector } from "../../../store/hooks"; -import { updateReduxState } from "../../../store/utils"; import { RoutePath } from "../../../routes"; +import { getNextRoute } from "../../../routes/nextRoute"; import { DataProps } from "../../../routes/nextRoute/nextRoute.types"; +import { useAppDispatch, useAppSelector } from "../../../store/hooks"; import { getSeedPhraseCache } from "../../../store/reducers/seedPhraseCache"; +import { getStateCache } from "../../../store/reducers/stateCache"; +import { updateReduxState } from "../../../store/utils"; +import { Alert as AlertConfirm } from "../../components/Alert"; import { ScrollablePageLayout } from "../../components/layout/ScrollablePageLayout"; -import { PageHeader } from "../../components/PageHeader"; import { PageFooter } from "../../components/PageFooter"; +import { PageHeader } from "../../components/PageHeader"; import { SeedPhraseModule } from "../../components/SeedPhraseModule"; +import { SwitchOnboardingModeModal } from "../../components/SwitchOnboardingModeModal"; +import { OnboardingMode } from "../../components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.types"; import { TermsModal } from "../../components/TermsModal"; import { useAppIonRouter } from "../../hooks"; -import { Agent } from "../../../core/agent/agent"; -import { BranAndMnemonic } from "../../../core/agent/agent.types"; -import { SwitchOnboardingMode } from "../../components/SwitchOnboardingMode"; -import { OnboardingMode } from "../../components/SwitchOnboardingMode/SwitchOnboardingMode.types"; import { showError } from "../../utils/error"; +import { RecoverySeedPhraseDocumentModal } from "./components/RecoverySeedPhraseDocumentModal"; +import "./GenerateSeedPhrase.scss"; const GenerateSeedPhrase = () => { const pageId = "generate-seed-phrase"; @@ -38,6 +40,8 @@ const GenerateSeedPhrase = () => { const [termsModalIsOpen, setTermsModalIsOpen] = useState(false); const [privacyModalIsOpen, setPrivacyModalIsOpen] = useState(false); const [checked, setChecked] = useState(false); + const [openDocument, setOpenDocument] = useState(false); + const [showSwitchModeModal, setSwitchModeModal] = useState(false); const initializeSeedPhrase = async () => { setHideSeedPhrase(true); @@ -127,19 +131,27 @@ const GenerateSeedPhrase = () => {

{i18n.t("generateseedphrase.onboarding.title")}

-

- {i18n.t("generateseedphrase.onboarding.paragraph.top")} -

+ setOpenDocument(true)} + fill="outline" + data-testid="recovery-phrase-docs-btn" + className="switch-button secondary-button" + > + + {i18n.t("generateseedphrase.onboarding.button.recoverydocumentation")} +

{i18n.t("generateseedphrase.onboarding.paragraph.bottom")}

-
{ />

- - { setAlertConfirmIsOpen(true); }} primaryButtonDisabled={hideSeedPhrase || !checked} + tertiaryButtonText={`${i18n.t("generateseedphrase.onboarding.button.switch")}`} + tertiaryButtonAction={() => setSwitchModeModal(true)} + tertiaryButtonIcon={refreshOutline} + /> + + { )}`} actionConfirm={handleContinue} /> + + ); }; diff --git a/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/DocumentSection.tsx b/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/DocumentSection.tsx new file mode 100644 index 000000000..9f273d341 --- /dev/null +++ b/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/DocumentSection.tsx @@ -0,0 +1,20 @@ +import { Trans } from "react-i18next"; +import { i18n } from "../../../../../i18n" +import { Content, DocumentSectionProps } from "./RecoverySeedPhraseDocumentModal.types" + +const DocumentSection = ({ sectionKey, image }: DocumentSectionProps) => { + const title = i18n.t(`generateseedphrase.onboarding.recoveryseedphrasedocs.content.${sectionKey}.title`); + const content: Content[] = i18n.t(`generateseedphrase.onboarding.recoveryseedphrasedocs.content.${sectionKey}.content`, { + returnObjects: true + }); + + return
+

{title}

+ {image && {title}} + { + content.map(({text}, index) =>

{text}

) + } +
+} + +export { DocumentSection }; \ No newline at end of file diff --git a/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/RecoverySeedPhraseDocumentModal.scss b/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/RecoverySeedPhraseDocumentModal.scss new file mode 100644 index 000000000..3ec490d51 --- /dev/null +++ b/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/RecoverySeedPhraseDocumentModal.scss @@ -0,0 +1,17 @@ +.recovery-seedphrase-docs-modal { + .info-card { + margin-top: 0.25rem; + } + + .document-section { + .image { + margin: 0.75rem auto 0; + display: block; + } + + .content { + margin: 0.75rem 0 0; + font-weight: 500; + } + } +} \ No newline at end of file diff --git a/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/RecoverySeedPhraseDocumentModal.tsx b/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/RecoverySeedPhraseDocumentModal.tsx new file mode 100644 index 000000000..94d2307eb --- /dev/null +++ b/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/RecoverySeedPhraseDocumentModal.tsx @@ -0,0 +1,39 @@ +import { IonModal } from "@ionic/react"; +import { RecoverySeedPhraseDocumentModalProps } from "./RecoverySeedPhraseDocumentModal.types"; +import { ScrollablePageLayout } from "../../../../components/layout/ScrollablePageLayout"; +import { PageHeader } from "../../../../components/PageHeader"; +import { i18n } from "../../../../../i18n"; +import { InfoCard } from "../../../../components/InfoCard"; +import { DocumentSection } from "./DocumentSection"; +import Image from "../../../../assets/images/SeedPhraseDocs.png"; +import "./RecoverySeedPhraseDocumentModal.scss"; + +const RecoverySeedPhraseDocumentModal = ({isOpen, setIsOpen}: RecoverySeedPhraseDocumentModalProps) => { + return ( + setIsOpen(false)} + > + setIsOpen(false)} + title={`${i18n.t("generateseedphrase.onboarding.recoveryseedphrasedocs.title")}`} + /> + } + > + + + + + + + ) +} + +export { RecoverySeedPhraseDocumentModal }; \ No newline at end of file diff --git a/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/RecoverySeedPhraseDocumentModal.types.ts b/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/RecoverySeedPhraseDocumentModal.types.ts new file mode 100644 index 000000000..c92500a1b --- /dev/null +++ b/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/RecoverySeedPhraseDocumentModal.types.ts @@ -0,0 +1,15 @@ +interface RecoverySeedPhraseDocumentModalProps { + isOpen: boolean; + setIsOpen: (value: boolean) => void; +} + +interface DocumentSectionProps { + sectionKey: string; + image?: string; +} + +interface Content { + text: string; +} + +export type { RecoverySeedPhraseDocumentModalProps, DocumentSectionProps, Content }; \ No newline at end of file diff --git a/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/index.ts b/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/index.ts new file mode 100644 index 000000000..ab01fdfb5 --- /dev/null +++ b/src/ui/pages/GenerateSeedPhrase/components/RecoverySeedPhraseDocumentModal/index.ts @@ -0,0 +1 @@ +export * from "./RecoverySeedPhraseDocumentModal"; \ No newline at end of file diff --git a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx index 9c3ab62d0..7b0623fa6 100644 --- a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx +++ b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx @@ -357,7 +357,7 @@ describe("Wallet connect: empty history", () => { }, identifiersCache: { identifiers: { - "EFn1HAaIyISfu_pwLA8DFgeKxr0pLzBccb4eXHSPVQ6L" : { + "EFn1HAaIyISfu_pwLA8DFgeKxr0pLzBccb4eXHSPVQ6L": { displayName: "ms", id: "EFn1HAaIyISfu_pwLA8DFgeKxr0pLzBccb4eXHSPVQ6L", createdAtUTC: "2024-07-25T13:33:20.323Z", @@ -519,7 +519,7 @@ describe("Wallet connect", () => { const deleteConfirmButton = await findByText(EN_TRANSLATIONS.tabs.menu.tab.items.connectwallet.connectionhistory.deletealert.confirm) - + fireEvent.click(deleteConfirmButton); await waitFor(() => { @@ -623,7 +623,7 @@ describe("Wallet connect", () => { }); test("Connect wallet", async () => { - const { getByText, getByTestId, queryByText } = render( + const { getByText, getByTestId, queryByText, getAllByText } = render( @@ -657,14 +657,12 @@ describe("Wallet connect", () => { ).toBeVisible(); }); - act(() => { - fireEvent.click( - getByText( - EN_TRANSLATIONS.tabs.menu.tab.items.connectwallet - .disconnectbeforecreatealert.confirm - ) - ); - }); + fireEvent.click( + getAllByText( + EN_TRANSLATIONS.tabs.menu.tab.items.connectwallet + .disconnectbeforecreatealert.confirm + )[1] + ); await waitFor(() => { expect( @@ -791,7 +789,7 @@ describe("Wallet connect", () => { }, identifiersCache: { identifiers: { - "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRd" : { + "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRd": { id: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRd", displayName: "Professional ID", createdAtUTC: "2023-01-01T19:23:24Z", @@ -860,7 +858,7 @@ describe("Wallet connect", () => { }, identifiersCache: { identifiers: { - "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRd" : { + "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRd": { id: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRd", displayName: "Professional ID", createdAtUTC: "2023-01-01T19:23:24Z", diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx index d9cc1e21a..2b2fbbe9a 100644 --- a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx +++ b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx @@ -148,4 +148,25 @@ describe("Verify Recovery Seed Phrase", () => { ); }); }); + + test("Show switch onboarding modal", async () => { + const history = createMemoryHistory(); + history.push(RoutePath.VERIFY_RECOVERY_SEED_PHRASE); + const { getByText, getByTestId } = render( + + + + + + ); + + fireEvent.click(getByTestId("tertiary-button-verify-recovery-seed-phrase")); + + await waitFor(() => { + expect(getByText(TRANSLATIONS.switchmodemodal.title)).toBeVisible(); + }) + }); }); diff --git a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.scss b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.scss index 3e698466b..e2904268a 100644 --- a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.scss +++ b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.scss @@ -9,7 +9,7 @@ display: flex; flex-direction: column; justify-content: space-between; - min-height: calc(100vh - (4rem + var(--ion-safe-area-top))); + min-height: calc(100vh - (4.5rem + var(--ion-safe-area-top))); } .seed-phrase-module:nth-of-type(1) { @@ -33,7 +33,7 @@ .clear-button { display: flex; width: fit-content; - margin: 1.25rem auto 0; + margin: 1.25rem auto 1rem; } @media screen and (min-width: 250px) and (max-width: 370px) { diff --git a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx index bba8efd94..001066528 100644 --- a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx +++ b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx @@ -64,7 +64,7 @@ describe("Verify Seed Phrase Page", () => { const history = createMemoryHistory(); history.push(RoutePath.GENERATE_SEED_PHRASE); - const { getByTestId, queryByText, getByText, findByText } = render( + const { getByTestId, queryByText, findByText } = render( { expect(seedNumberElements.length).toBe(0); }); }); + test("Show switch onboarding modal", async () => { + const history = createMemoryHistory(); + history.push(RoutePath.VERIFY_SEED_PHRASE); + const { getByTestId, getByText } = render( + + + + + + ); + + fireEvent.click(getByTestId("tertiary-button-verify-seed-phrase")); + + await waitFor(() => { + expect(getByText(EN_TRANSLATIONS.switchmodemodal.title)).toBeVisible(); + }) + }); }); diff --git a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.tsx b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.tsx index 5a9f6ae73..1abc3b52a 100644 --- a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.tsx +++ b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.tsx @@ -1,5 +1,5 @@ import { IonButton, IonIcon } from "@ionic/react"; -import { closeOutline } from "ionicons/icons"; +import { closeOutline, refreshOutline } from "ionicons/icons"; import { useCallback, useEffect, useMemo, useState } from "react"; import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; import { i18n } from "../../../i18n"; @@ -15,12 +15,12 @@ import { Alert as AlertFail } from "../../components/Alert"; import { PageFooter } from "../../components/PageFooter"; import { PageHeader } from "../../components/PageHeader"; import { SeedPhraseModule } from "../../components/SeedPhraseModule"; -import { SwitchOnboardingMode } from "../../components/SwitchOnboardingMode"; -import { OnboardingMode } from "../../components/SwitchOnboardingMode/SwitchOnboardingMode.types"; import { ScrollablePageLayout } from "../../components/layout/ScrollablePageLayout"; import { useAppIonRouter } from "../../hooks"; import "./VerifySeedPhrase.scss"; import { showError } from "../../utils/error"; +import { SwitchOnboardingModeModal } from "../../components/SwitchOnboardingModeModal"; +import { OnboardingMode } from "../../components/SwitchOnboardingModeModal/SwitchOnboardingModeModal.types"; const VerifySeedPhrase = () => { const pageId = "verify-seed-phrase"; @@ -32,6 +32,7 @@ const VerifySeedPhrase = () => { const [clearAlertOpen, setClearAlertOpen] = useState(false); const [alertIsOpen, setAlertIsOpen] = useState(false); const ionRouter = useAppIonRouter(); + const [showSwitchModeModal, setSwitchModeModal] = useState(false); const originalSeedPhrase = useMemo( () => seedPhraseStore.seedPhrase.split(" "), @@ -211,7 +212,6 @@ const VerifySeedPhrase = () => { {i18n.t("verifyseedphrase.onboarding.button.clear")} )} - { primaryButtonDisabled={ !(originalSeedPhrase.length == seedPhraseSelected.length) } + tertiaryButtonText={`${i18n.t("generateseedphrase.onboarding.button.switch")}`} + tertiaryButtonAction={() => setSwitchModeModal(true)} + tertiaryButtonIcon={refreshOutline} /> { actionCancel={() => setClearAlertOpen(false)} actionDismiss={() => setClearAlertOpen(false)} /> + ); };