From a273a77e077ea66cf7b906d36ee8e20409bfbc59 Mon Sep 17 00:00:00 2001 From: Oleg Chendighelean Date: Tue, 3 Sep 2024 15:35:01 +0100 Subject: [PATCH] Add verification flow --- apps/web/src/assets/icons/index.ts | 5 +- apps/web/src/assets/icons/pencil.svg | 5 + apps/web/src/assets/icons/scan.svg | 5 + apps/web/src/assets/icons/window-close.svg | 8 ++ .../components/AccountCard/AccountBalance.tsx | 2 +- .../components/AccountCard/SendTezButton.tsx | 2 +- .../AccountSelectorModal.tsx | 2 +- .../components/EmptyMessage/VerifyMessage.tsx | 48 ------- apps/web/src/components/EmptyMessage/index.ts | 2 +- .../Menu/AdvancedMenu/AdvancedMenu.tsx | 2 +- apps/web/src/components/Menu/Menu.tsx | 2 +- .../OnboardOptions/OnboardOptions.tsx | 2 +- .../SetupPassword/SetupPassword.tsx | 36 +++-- .../VerificationFlow/ImportantNoticeModal.tsx | 96 +++++++++++++ .../RecordSeedphraseModal.tsx} | 40 ++++-- .../VerificationInfoModal.tsx | 10 +- .../VerificationFlow}/VerifyMessage.test.tsx | 4 +- .../VerificationFlow/VerifyMessage.tsx | 36 +++++ .../VerifySeedphraseModal.test.tsx | 106 ++++++++++++++ .../VerifySeedphraseModal.tsx | 131 ++++++++++++++++++ .../Onboarding/VerificationFlow/index.ts | 6 + .../VerificationFlow/useHandleVerify.tsx | 34 +++++ .../useIsAccountVerified.tsx | 0 .../components/ViewOverlay/ViewOverlay.tsx | 2 +- apps/web/src/views/Activity/Activity.tsx | 5 +- apps/web/src/views/NFTs/NFTs.tsx | 5 +- apps/web/src/views/Tokens/Tokens.tsx | 5 +- packages/state/src/reducer.ts | 1 + packages/state/src/slices/accounts/State.ts | 1 + .../state/src/slices/accounts/accounts.ts | 4 + 30 files changed, 515 insertions(+), 92 deletions(-) create mode 100644 apps/web/src/assets/icons/pencil.svg create mode 100644 apps/web/src/assets/icons/scan.svg create mode 100644 apps/web/src/assets/icons/window-close.svg delete mode 100644 apps/web/src/components/EmptyMessage/VerifyMessage.tsx create mode 100644 apps/web/src/components/Onboarding/VerificationFlow/ImportantNoticeModal.tsx rename apps/web/src/components/Onboarding/{CopySeedphraseModal.tsx => VerificationFlow/RecordSeedphraseModal.tsx} (54%) rename apps/web/src/components/Onboarding/{ => VerificationFlow}/VerificationInfoModal.tsx (87%) rename apps/web/src/components/{EmptyMessage => Onboarding/VerificationFlow}/VerifyMessage.test.tsx (89%) create mode 100644 apps/web/src/components/Onboarding/VerificationFlow/VerifyMessage.tsx create mode 100644 apps/web/src/components/Onboarding/VerificationFlow/VerifySeedphraseModal.test.tsx create mode 100644 apps/web/src/components/Onboarding/VerificationFlow/VerifySeedphraseModal.tsx create mode 100644 apps/web/src/components/Onboarding/VerificationFlow/index.ts create mode 100644 apps/web/src/components/Onboarding/VerificationFlow/useHandleVerify.tsx rename apps/web/src/components/Onboarding/{ => VerificationFlow}/useIsAccountVerified.tsx (100%) diff --git a/apps/web/src/assets/icons/index.ts b/apps/web/src/assets/icons/index.ts index a7dc5f3ac7..320d0b0dc8 100644 --- a/apps/web/src/assets/icons/index.ts +++ b/apps/web/src/assets/icons/index.ts @@ -10,10 +10,10 @@ export { default as ChevronDownIcon } from "./chevron-down.svg"; export { default as ChevronRightIcon } from "./chevron-right.svg"; export { default as CloseIcon } from "./close.svg"; export { default as CodeSandboxIcon } from "./code-sandbox.svg"; +export { default as CoinIcon } from "./coin.svg"; export { default as ContactIcon } from "./contact-s.svg"; export { default as ContractIcon } from "./contract.svg"; export { default as CopyIcon } from "./copy.svg"; -export { default as CoinIcon } from "./coin.svg"; export { default as CrossedCircleIcon } from "./crossed-circle.svg"; export { default as DelegateIcon } from "./delegate-s.svg"; export { default as DownloadIcon } from "./download.svg"; @@ -42,10 +42,12 @@ export { default as MenuIcon } from "./menu.svg"; export { default as MoonIcon } from "./moon.svg"; export { default as MultisigIcon } from "./multisig.svg"; export { default as OutgoingArrowIcon } from "./outgoing-arrow.svg"; +export { default as PencilIcon } from "./pencil.svg"; export { default as PlusIcon } from "./plus.svg"; export { default as PyramidIcon } from "./pyramid.svg"; export { default as RadioIcon } from "./radio.svg"; export { default as RedditIcon } from "./reddit.svg"; +export { default as ScanIcon } from "./scan.svg"; export { default as SearchIcon } from "./search.svg"; export { default as SelectorIcon } from "./selector.svg"; export { default as SettingsIcon } from "./settings.svg"; @@ -60,6 +62,7 @@ export { default as UserIcon } from "./user.svg"; export { default as UserPlusIcon } from "./user-plus.svg"; export { default as VerifiedIcon } from "./verified.svg"; export { default as WalletIcon } from "./wallet.svg"; +export { default as WindowCloseIcon } from "./window-close.svg"; export { TokenIcon } from "./TokenIcon"; import CloseIcon from "./close.svg"; diff --git a/apps/web/src/assets/icons/pencil.svg b/apps/web/src/assets/icons/pencil.svg new file mode 100644 index 0000000000..0d3958d99d --- /dev/null +++ b/apps/web/src/assets/icons/pencil.svg @@ -0,0 +1,5 @@ + + + diff --git a/apps/web/src/assets/icons/scan.svg b/apps/web/src/assets/icons/scan.svg new file mode 100644 index 0000000000..ce767b480b --- /dev/null +++ b/apps/web/src/assets/icons/scan.svg @@ -0,0 +1,5 @@ + + + diff --git a/apps/web/src/assets/icons/window-close.svg b/apps/web/src/assets/icons/window-close.svg new file mode 100644 index 0000000000..e113c889c2 --- /dev/null +++ b/apps/web/src/assets/icons/window-close.svg @@ -0,0 +1,8 @@ + + + + diff --git a/apps/web/src/components/AccountCard/AccountBalance.tsx b/apps/web/src/components/AccountCard/AccountBalance.tsx index 8ef4ab4645..ed5df57164 100644 --- a/apps/web/src/components/AccountCard/AccountBalance.tsx +++ b/apps/web/src/components/AccountCard/AccountBalance.tsx @@ -8,7 +8,7 @@ import { ArrowDownLeftIcon, WalletIcon } from "../../assets/icons"; import { useColor } from "../../styles/useColor"; import { AccountInfoModal } from "../AccountSelectorModal"; import { IconButtonWithText } from "../IconButtonWithText"; -import { useIsAccountVerified } from "../Onboarding/useIsAccountVerified"; +import { useIsAccountVerified } from "../Onboarding/VerificationFlow/useIsAccountVerified"; export const AccountBalance = () => { const color = useColor(); diff --git a/apps/web/src/components/AccountCard/SendTezButton.tsx b/apps/web/src/components/AccountCard/SendTezButton.tsx index e3252d5e6e..ffe006cff9 100644 --- a/apps/web/src/components/AccountCard/SendTezButton.tsx +++ b/apps/web/src/components/AccountCard/SendTezButton.tsx @@ -3,7 +3,7 @@ import { useCurrentAccount } from "@umami/state"; import { ArrowUpRightIcon } from "../../assets/icons"; import { IconButtonWithText } from "../IconButtonWithText/IconButtonWithText"; -import { useIsAccountVerified } from "../Onboarding/useIsAccountVerified"; +import { useIsAccountVerified } from "../Onboarding/VerificationFlow/useIsAccountVerified"; import { FormPage as SendTezFormPage } from "../SendFlow/Tez/FormPage"; export const SendTezButton = () => { diff --git a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx index 6c28c16bbc..6c162e428d 100644 --- a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx +++ b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx @@ -23,7 +23,7 @@ import { useColor } from "../../styles/useColor"; import { AccountTile } from "../AccountTile"; import { ModalCloseButton } from "../CloseButton"; import { OnboardOptionsModal } from "../Onboarding/OnboardOptions"; -import { useIsAccountVerified } from "../Onboarding/useIsAccountVerified"; +import { useIsAccountVerified } from "../Onboarding/VerificationFlow/useIsAccountVerified"; export const AccountSelectorModal = () => { const accounts = useImplicitAccounts(); diff --git a/apps/web/src/components/EmptyMessage/VerifyMessage.tsx b/apps/web/src/components/EmptyMessage/VerifyMessage.tsx deleted file mode 100644 index 61825af9c9..0000000000 --- a/apps/web/src/components/EmptyMessage/VerifyMessage.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Flex, Link } from "@chakra-ui/react"; -import { useDynamicModalContext } from "@umami/components"; -import { type MnemonicAccount } from "@umami/core"; -import { useCurrentAccount, useGetDecryptedMnemonic } from "@umami/state"; - -import { EmptyMessage, type EmptyMessageProps } from "./EmptyMessage"; -import { useColor } from "../../styles/useColor"; -import { CopySeedphraseModal } from "../Onboarding/CopySeedphraseModal"; -import { SetupPassword } from "../Onboarding/SetupPassword"; -import { VerificationInfoModal } from "../Onboarding/VerificationInfoModal"; - -export const VerifyMessage = ({ ...props }: Omit) => { - const { openWith } = useDynamicModalContext(); - const currentAccount = useCurrentAccount() as MnemonicAccount; - const color = useColor(); - const getDecryptedMnemonic = useGetDecryptedMnemonic(); - - const handleDeriveSeedphraseModal = async (password: string) => { - const mnemonic = await getDecryptedMnemonic(currentAccount, password); - - return openWith(); - }; - - return ( - - - openWith() - } - subtitle={ - "Please verify your account, to unlock all features\n and keep your account secure." - } - title="Verify Your Account" - {...props} - /> - openWith()} - textDecor="underline" - > - How does verification work? - - - ); -}; diff --git a/apps/web/src/components/EmptyMessage/index.ts b/apps/web/src/components/EmptyMessage/index.ts index 3913fd280f..af14d53027 100644 --- a/apps/web/src/components/EmptyMessage/index.ts +++ b/apps/web/src/components/EmptyMessage/index.ts @@ -1,2 +1,2 @@ export { EmptyMessage } from "./EmptyMessage"; -export { VerifyMessage } from "./VerifyMessage"; +export type { EmptyMessageProps } from "./EmptyMessage"; diff --git a/apps/web/src/components/Menu/AdvancedMenu/AdvancedMenu.tsx b/apps/web/src/components/Menu/AdvancedMenu/AdvancedMenu.tsx index 5760da093d..4b17f6de37 100644 --- a/apps/web/src/components/Menu/AdvancedMenu/AdvancedMenu.tsx +++ b/apps/web/src/components/Menu/AdvancedMenu/AdvancedMenu.tsx @@ -1,7 +1,7 @@ import { useDynamicDrawerContext } from "@umami/components"; import { AlertCircleIcon, LockIcon, RadioIcon } from "../../../assets/icons"; -import { useIsAccountVerified } from "../../Onboarding/useIsAccountVerified"; +import { useIsAccountVerified } from "../../Onboarding/VerificationFlow/useIsAccountVerified"; import { ChangePasswordMenu } from "../ChangePasswordMenu/ChangePasswordMenu"; import { ErrorLogsMenu } from "../ErrorLogsMenu/ErrorLogsMenu"; import { GenericMenu } from "../GenericMenu"; diff --git a/apps/web/src/components/Menu/Menu.tsx b/apps/web/src/components/Menu/Menu.tsx index 3339866e49..0943c4f7b4 100644 --- a/apps/web/src/components/Menu/Menu.tsx +++ b/apps/web/src/components/Menu/Menu.tsx @@ -18,7 +18,7 @@ import { UserPlusIcon, } from "../../assets/icons"; import { OnboardOptionsModal } from "../Onboarding/OnboardOptions"; -import { useIsAccountVerified } from "../Onboarding/useIsAccountVerified"; +import { useIsAccountVerified } from "../Onboarding/VerificationFlow/useIsAccountVerified"; export const Menu = () => { const { openWith: openModal } = useDynamicModalContext(); diff --git a/apps/web/src/components/Onboarding/OnboardOptions/OnboardOptions.tsx b/apps/web/src/components/Onboarding/OnboardOptions/OnboardOptions.tsx index b71811f422..fd192d7ed7 100644 --- a/apps/web/src/components/Onboarding/OnboardOptions/OnboardOptions.tsx +++ b/apps/web/src/components/Onboarding/OnboardOptions/OnboardOptions.tsx @@ -17,7 +17,7 @@ export const OnboardOptions = ({ children }: PropsWithChildren) => { const { openWith } = useDynamicModalContext(); return ( - + Continue with: diff --git a/apps/web/src/components/Onboarding/SetupPassword/SetupPassword.tsx b/apps/web/src/components/Onboarding/SetupPassword/SetupPassword.tsx index 994f44bf79..954bba3c1d 100644 --- a/apps/web/src/components/Onboarding/SetupPassword/SetupPassword.tsx +++ b/apps/web/src/components/Onboarding/SetupPassword/SetupPassword.tsx @@ -14,7 +14,9 @@ import { type Curves } from "@taquito/signer"; import { useDynamicModalContext, useMultiForm } from "@umami/components"; import { DEFAULT_ACCOUNT_LABEL } from "@umami/core"; import { + accountsActions, generate24WordMnemonic, + useAppDispatch, useAsyncActionHandler, useGetNextAvailableAccountLabels, useIsPasswordSet, @@ -42,11 +44,14 @@ type FormFields = { type Mode = "mnemonic" | "secret_key" | "new_mnemonic"; type SetupPasswordProps = { - mode: Mode; - handleSubmit?: (password: string) => void; + mode?: Mode; + handleProceedToVerification?: (password: string) => void; }; -export const SetupPassword = ({ mode, handleSubmit }: SetupPasswordProps) => { +export const SetupPassword = ({ + mode, + handleProceedToVerification: handleProceedToVerification, +}: SetupPasswordProps) => { const color = useColor(); const { handleAsyncAction, isLoading } = useAsyncActionHandler(); const { allFormValues, onClose } = useDynamicModalContext(); @@ -55,6 +60,7 @@ export const SetupPassword = ({ mode, handleSubmit }: SetupPasswordProps) => { const checkPassword = useValidateMasterPassword(); const getNextAvailableAccountLabels = useGetNextAvailableAccountLabels(); const isPasswordSet = useIsPasswordSet(); + const dispatch = useAppDispatch(); const form = useMultiForm({ mode: "onBlur", @@ -71,6 +77,18 @@ export const SetupPassword = ({ mode, handleSubmit }: SetupPasswordProps) => { const isNewMnemonic = mode === "new_mnemonic"; + const onHandleSubmit = (formFields: FormFields) => { + if (handleProceedToVerification) { + return handleProceedToVerification(formFields.password); + } + + if (isNewMnemonic) { + dispatch(accountsActions.setPassword(formFields.password)); + } + + return onSubmit(formFields); + }; + const onSubmit = ({ password, curve, derivationPath }: FormFields) => handleAsyncAction(async () => { const label = getNextAvailableAccountLabels(DEFAULT_ACCOUNT_LABEL)[0]; @@ -102,14 +120,16 @@ export const SetupPassword = ({ mode, handleSubmit }: SetupPasswordProps) => { }); break; } + default: + break; } return onClose(); }); const icon = isNewMnemonic ? UserIcon : LockIcon; - const title = isNewMnemonic ? "Create Password" : "Almost there"; - const buttonLabel = isNewMnemonic ? "Create Account" : "Import Wallet"; + const title = mode ? (isNewMnemonic ? "Create Password" : "Almost there") : "Confirm password"; + const buttonLabel = mode ? (isNewMnemonic ? "Create Account" : "Import Wallet") : "Confirm"; return ( @@ -134,11 +154,7 @@ export const SetupPassword = ({ mode, handleSubmit }: SetupPasswordProps) => { -
- handleSubmit ? handleSubmit(formFields.password) : onSubmit(formFields) - )} - > + { + const color = useColor(); + const { openWith } = useDynamicModalContext(); + + return ( + + + +
+ + Important Notice + + Please read the following before you continue to see your secret Seed Phrase. + +
+ +
+ + + {items.map(({ text, icon }) => ( + + + {text} + + ))} + + + + + +
+ ); +}; diff --git a/apps/web/src/components/Onboarding/CopySeedphraseModal.tsx b/apps/web/src/components/Onboarding/VerificationFlow/RecordSeedphraseModal.tsx similarity index 54% rename from apps/web/src/components/Onboarding/CopySeedphraseModal.tsx rename to apps/web/src/components/Onboarding/VerificationFlow/RecordSeedphraseModal.tsx index 52a87ad96a..fe46ae2e29 100644 --- a/apps/web/src/components/Onboarding/CopySeedphraseModal.tsx +++ b/apps/web/src/components/Onboarding/VerificationFlow/RecordSeedphraseModal.tsx @@ -12,19 +12,22 @@ import { ModalHeader, Text, } from "@chakra-ui/react"; +import { useDynamicModalContext } from "@umami/components"; -import { CopyIcon, KeyIcon } from "../../assets/icons"; -import { useColor } from "../../styles/useColor"; -import { ModalBackButton } from "../BackButton"; -import { ModalCloseButton } from "../CloseButton"; -import { CopyButton } from "../CopyButton"; +import { VerifySeedphraseModal } from "./VerifySeedphraseModal"; +import { CopyIcon, KeyIcon } from "../../../assets/icons"; +import { useColor } from "../../../styles/useColor"; +import { ModalBackButton } from "../../BackButton"; +import { ModalCloseButton } from "../../CloseButton"; +import { CopyButton } from "../../CopyButton"; type CopySeedphraseModalProps = { seedPhrase: string; }; -export const CopySeedphraseModal = ({ seedPhrase }: CopySeedphraseModalProps) => { +export const RecordSeedphraseModal = ({ seedPhrase }: CopySeedphraseModalProps) => { const color = useColor(); + const { openWith } = useDynamicModalContext(); const words = seedPhrase.split(" "); return ( @@ -33,7 +36,7 @@ export const CopySeedphraseModal = ({ seedPhrase }: CopySeedphraseModalProps) =>
- + Import Wallet Record these 24 words in order to restore your wallet in the future @@ -42,23 +45,28 @@ export const CopySeedphraseModal = ({ seedPhrase }: CopySeedphraseModalProps) => - + {words.map((word, index) => ( - + {String(index + 1).padStart(2, "0")}. - + {word} @@ -70,7 +78,13 @@ export const CopySeedphraseModal = ({ seedPhrase }: CopySeedphraseModalProps) => - diff --git a/apps/web/src/components/Onboarding/VerificationInfoModal.tsx b/apps/web/src/components/Onboarding/VerificationFlow/VerificationInfoModal.tsx similarity index 87% rename from apps/web/src/components/Onboarding/VerificationInfoModal.tsx rename to apps/web/src/components/Onboarding/VerificationFlow/VerificationInfoModal.tsx index d1deee2adb..3f23f4242b 100644 --- a/apps/web/src/components/Onboarding/VerificationInfoModal.tsx +++ b/apps/web/src/components/Onboarding/VerificationFlow/VerificationInfoModal.tsx @@ -14,9 +14,10 @@ import { Text, } from "@chakra-ui/react"; -import { AlertIcon } from "../../assets/icons"; -import { useColor } from "../../styles/useColor"; -import { ModalCloseButton } from "../CloseButton"; +import { useHandleVerify } from "./useHandleVerify"; +import { AlertIcon } from "../../../assets/icons"; +import { useColor } from "../../../styles/useColor"; +import { ModalCloseButton } from "../../CloseButton"; // TODO: Replace with actual copy paste const accordionItems = [ @@ -38,6 +39,7 @@ const accordionItems = [ export const VerificationInfoModal = () => { const color = useColor(); + const handleVerify = useHandleVerify(); return ( @@ -78,7 +80,7 @@ export const VerificationInfoModal = () => { - diff --git a/apps/web/src/components/EmptyMessage/VerifyMessage.test.tsx b/apps/web/src/components/Onboarding/VerificationFlow/VerifyMessage.test.tsx similarity index 89% rename from apps/web/src/components/EmptyMessage/VerifyMessage.test.tsx rename to apps/web/src/components/Onboarding/VerificationFlow/VerifyMessage.test.tsx index a0d4c64a8e..29a4f460e3 100644 --- a/apps/web/src/components/EmptyMessage/VerifyMessage.test.tsx +++ b/apps/web/src/components/Onboarding/VerificationFlow/VerifyMessage.test.tsx @@ -1,6 +1,6 @@ +import { VerificationInfoModal } from "./VerificationInfoModal"; import { VerifyMessage } from "./VerifyMessage"; -import { dynamicModalContextMock, render, screen, userEvent } from "../../testUtils"; -import { VerificationInfoModal } from "../Onboarding/VerificationInfoModal"; +import { dynamicModalContextMock, render, screen, userEvent } from "../../../testUtils"; describe("", () => { it("renders correctly", () => { diff --git a/apps/web/src/components/Onboarding/VerificationFlow/VerifyMessage.tsx b/apps/web/src/components/Onboarding/VerificationFlow/VerifyMessage.tsx new file mode 100644 index 0000000000..ed5cdc6fbd --- /dev/null +++ b/apps/web/src/components/Onboarding/VerificationFlow/VerifyMessage.tsx @@ -0,0 +1,36 @@ +import { Flex, Link } from "@chakra-ui/react"; +import { useDynamicModalContext } from "@umami/components"; + +import { useHandleVerify } from "./useHandleVerify"; +import { VerificationInfoModal } from "./VerificationInfoModal"; +import { useColor } from "../../../styles/useColor"; +import { EmptyMessage, type EmptyMessageProps } from "../../EmptyMessage"; + +export const VerifyMessage = ({ ...props }: Omit) => { + const color = useColor(); + const { openWith } = useDynamicModalContext(); + const handleVerify = useHandleVerify(); + + return ( + + + openWith()} + textDecor="underline" + > + How does verification work? + + + ); +}; diff --git a/apps/web/src/components/Onboarding/VerificationFlow/VerifySeedphraseModal.test.tsx b/apps/web/src/components/Onboarding/VerificationFlow/VerifySeedphraseModal.test.tsx new file mode 100644 index 0000000000..36d0542e09 --- /dev/null +++ b/apps/web/src/components/Onboarding/VerificationFlow/VerifySeedphraseModal.test.tsx @@ -0,0 +1,106 @@ +import { selectRandomElements } from "@umami/core"; +import { mnemonic1 } from "@umami/test-utils"; + +import { VerifySeedphraseModal } from "./VerifySeedphraseModal"; +import { act, fireEvent, render, screen, userEvent, waitFor } from "../../../testUtils"; + +jest.mock("@umami/core", () => ({ + ...jest.requireActual("@umami/core"), + selectRandomElements: jest.fn(), +})); + +const selectRandomElementsMock = jest.mocked(selectRandomElements); + +beforeEach(() => { + const splitted = mnemonic1.split(" ").map((value, index) => ({ + index, + value, + })); + selectRandomElementsMock.mockReturnValue(splitted.slice(0, 5)); +}); + +const seedPhrase = mnemonic1; + +const fixture = () => ; + +describe("", () => { + test("when no mnemonic has been entered the button is disabled", () => { + render(fixture()); + + expect(screen.getByRole("button", { name: "Continue" })).toBeDisabled(); + }); + + test("when an invalid mnemonic has been entered the button is disabled", async () => { + const user = userEvent.setup(); + + render(fixture()); + + const inputFields = screen.getAllByRole("textbox"); + for (const input of inputFields) { + await act(() => user.type(input, "test")); + } + + expect(screen.getByRole("button", { name: "Continue" })).toBeDisabled(); + }); + + test("validation is working with all invalid", async () => { + render(fixture()); + const inputFields = screen.getAllByRole("textbox"); + inputFields.forEach(input => { + fireEvent.change(input, { target: { value: "test" } }); + fireEvent.blur(input); + }); + + await waitFor(() => { + expect(screen.getAllByText("Word doesn't match").length).toBe(5); + }); + }); + + test("validation is working with some invalid", async () => { + render(fixture()); + const inputFields = screen.getAllByRole("textbox"); + + // Enter correct value + fireEvent.change(inputFields[0], { + target: { value: mnemonic1.split(" ")[0] }, + }); + fireEvent.blur(inputFields[0]); + + // Enter incorrect values + inputFields.forEach(input => { + fireEvent.change(input, { target: { value: "test" } }); + fireEvent.blur(input); + }); + await waitFor(() => { + expect(screen.getAllByText("Word doesn't match").length).toBe(5); + }); + }); + + test("validation is working with all valid", async () => { + render(fixture()); + const inputFields = screen.getAllByRole("textbox"); + + // Enter correct value + const splitted = mnemonic1.split(" "); + + // Enter incorrect values + inputFields.forEach((input, index) => { + fireEvent.change(input, { target: { value: splitted[index] } }); + fireEvent.blur(input); + }); + + const confirmBtn = screen.getByRole("button", { name: "Continue" }); + + await waitFor(() => { + expect(confirmBtn).toBeEnabled(); + }); + + fireEvent.click(confirmBtn); + await waitFor(() => { + expect(goToStepMock).toHaveBeenCalledWith({ + type: "nameAccount", + account: { type: "mnemonic", mnemonic: mnemonic1 }, + }); + }); + }); +}); diff --git a/apps/web/src/components/Onboarding/VerificationFlow/VerifySeedphraseModal.tsx b/apps/web/src/components/Onboarding/VerificationFlow/VerifySeedphraseModal.tsx new file mode 100644 index 0000000000..69bc033f4b --- /dev/null +++ b/apps/web/src/components/Onboarding/VerificationFlow/VerifySeedphraseModal.tsx @@ -0,0 +1,131 @@ +import { + Button, + Center, + Flex, + FormControl, + FormErrorMessage, + Heading, + Icon, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Text, +} from "@chakra-ui/react"; +import { MnemonicAutocomplete, useDynamicModalContext } from "@umami/components"; +import { selectRandomElements } from "@umami/core"; +import { accountsActions, useAppDispatch, useCurrentAccount } from "@umami/state"; +import { useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; + +import { KeyIcon } from "../../../assets/icons"; +import { IS_DEV } from "../../../env"; +import { useColor } from "../../../styles/useColor"; +import { ModalBackButton } from "../../BackButton"; +import { ModalCloseButton } from "../../CloseButton"; + +type VerifySeedphraseModalProps = { + seedPhrase: string; +}; + +export const VerifySeedphraseModal = ({ seedPhrase }: VerifySeedphraseModalProps) => { + const color = useColor(); + const dispatch = useAppDispatch(); + const currentAccount = useCurrentAccount()!; + const { onClose } = useDynamicModalContext(); + const form = useForm({ + mode: "onBlur", + }); + const { + handleSubmit, + formState: { errors, isValid }, + } = form; + + const seedphraseArray = seedPhrase.split(" "); + const [randomElements] = useState(selectRandomElements(seedphraseArray, 3)); + + const onSubmit = () => { + dispatch( + accountsActions.setIsVerified({ + pkh: currentAccount.address.pkh, + isVerified: true, + }) + ); + + onClose(); + }; + + return ( + + + + +
+ + Verify Seed Phrase + + To verify, please type in the word that corresponds to each sequence number. + +
+
+ + + + + {randomElements.map(({ index, value }) => { + const inputName = `${index}`; + const error = errors[inputName]; + + return ( + + + + {String(index + 1).padStart(2, "0")}. + + { + if (_value !== value) { + return "Word doesn't match"; + } + }} + /> + + {error?.message && ( + {(error as any).message} + )} + + ); + })} + + + + + + {IS_DEV && ( + + )} + + + +
+ ); +}; diff --git a/apps/web/src/components/Onboarding/VerificationFlow/index.ts b/apps/web/src/components/Onboarding/VerificationFlow/index.ts new file mode 100644 index 0000000000..9cbe3fc17b --- /dev/null +++ b/apps/web/src/components/Onboarding/VerificationFlow/index.ts @@ -0,0 +1,6 @@ +export { RecordSeedphraseModal } from "./RecordSeedphraseModal"; +export { VerificationInfoModal } from "./VerificationInfoModal"; +export { VerifyMessage } from "./VerifyMessage"; +export { useIsAccountVerified } from "./useIsAccountVerified"; +export { ImportantNoticeModal } from "./ImportantNoticeModal"; +export { VerifySeedphraseModal } from "./VerifySeedphraseModal"; diff --git a/apps/web/src/components/Onboarding/VerificationFlow/useHandleVerify.tsx b/apps/web/src/components/Onboarding/VerificationFlow/useHandleVerify.tsx new file mode 100644 index 0000000000..aa76ab9188 --- /dev/null +++ b/apps/web/src/components/Onboarding/VerificationFlow/useHandleVerify.tsx @@ -0,0 +1,34 @@ +import { useDynamicModalContext } from "@umami/components"; +import { type MnemonicAccount } from "@umami/core"; +import { + useAppSelector, + useAsyncActionHandler, + useCurrentAccount, + useGetDecryptedMnemonic, +} from "@umami/state"; + +import { ImportantNoticeModal } from "./ImportantNoticeModal"; +import { SetupPassword } from "../SetupPassword"; + +export const useHandleVerify = () => { + const { openWith } = useDynamicModalContext(); + const currentAccount = useCurrentAccount() as MnemonicAccount; + const getDecryptedMnemonic = useGetDecryptedMnemonic(); + const { handleAsyncAction } = useAsyncActionHandler(); + const { password: masterPassword } = useAppSelector(state => state.accounts); + + const handleProceedToVerification = async (password: string) => + handleAsyncAction(async () => { + const mnemonic = await getDecryptedMnemonic(currentAccount, password); + + return openWith(, { size: "xl" }); + }); + + return () => { + if (masterPassword) { + return handleProceedToVerification(masterPassword); + } + + return openWith(); + }; +}; diff --git a/apps/web/src/components/Onboarding/useIsAccountVerified.tsx b/apps/web/src/components/Onboarding/VerificationFlow/useIsAccountVerified.tsx similarity index 100% rename from apps/web/src/components/Onboarding/useIsAccountVerified.tsx rename to apps/web/src/components/Onboarding/VerificationFlow/useIsAccountVerified.tsx diff --git a/apps/web/src/components/ViewOverlay/ViewOverlay.tsx b/apps/web/src/components/ViewOverlay/ViewOverlay.tsx index a82bbef558..9bda0b5a8b 100644 --- a/apps/web/src/components/ViewOverlay/ViewOverlay.tsx +++ b/apps/web/src/components/ViewOverlay/ViewOverlay.tsx @@ -2,7 +2,7 @@ import { Box, Icon } from "@chakra-ui/react"; import { CoinIcon, LockIcon, PyramidIcon, WalletIcon } from "../../assets/icons"; import { useColor } from "../../styles/useColor"; -import { useIsAccountVerified } from "../Onboarding/useIsAccountVerified"; +import { useIsAccountVerified } from "../Onboarding/VerificationFlow/useIsAccountVerified"; type ViewOverlayProps = { iconType: "activity" | "earn" | "nfts" | "tokens"; diff --git a/apps/web/src/views/Activity/Activity.tsx b/apps/web/src/views/Activity/Activity.tsx index bc40faf298..49066e4a5a 100644 --- a/apps/web/src/views/Activity/Activity.tsx +++ b/apps/web/src/views/Activity/Activity.tsx @@ -6,8 +6,9 @@ import { type UIEvent, useRef } from "react"; import loadingDots from "../../assets/loading-dots.gif"; import loadingWheel from "../../assets/loading-wheel.gif"; -import { EmptyMessage, VerifyMessage } from "../../components/EmptyMessage"; -import { useIsAccountVerified } from "../../components/Onboarding/useIsAccountVerified"; +import { EmptyMessage } from "../../components/EmptyMessage"; +import { VerifyMessage } from "../../components/Onboarding/VerificationFlow"; +import { useIsAccountVerified } from "../../components/Onboarding/VerificationFlow/useIsAccountVerified"; import { OperationTile } from "../../components/OperationTile"; import { ViewOverlay } from "../../components/ViewOverlay/ViewOverlay"; import { useColor } from "../../styles/useColor"; diff --git a/apps/web/src/views/NFTs/NFTs.tsx b/apps/web/src/views/NFTs/NFTs.tsx index 12d48d805d..6ac12582a5 100644 --- a/apps/web/src/views/NFTs/NFTs.tsx +++ b/apps/web/src/views/NFTs/NFTs.tsx @@ -7,8 +7,9 @@ import { range } from "lodash"; import { NFTCard } from "./NFTCard"; import { NFTDrawer } from "./NFTDrawer"; import { NFTFilter, useNFTFilter } from "./NFTFilter"; -import { EmptyMessage, VerifyMessage } from "../../components/EmptyMessage"; -import { useIsAccountVerified } from "../../components/Onboarding/useIsAccountVerified"; +import { EmptyMessage } from "../../components/EmptyMessage"; +import { VerifyMessage } from "../../components/Onboarding/VerificationFlow"; +import { useIsAccountVerified } from "../../components/Onboarding/VerificationFlow/useIsAccountVerified"; import { ViewOverlay } from "../../components/ViewOverlay/ViewOverlay"; import { useColor } from "../../styles/useColor"; diff --git a/apps/web/src/views/Tokens/Tokens.tsx b/apps/web/src/views/Tokens/Tokens.tsx index b3c0e854c8..5c9f167957 100644 --- a/apps/web/src/views/Tokens/Tokens.tsx +++ b/apps/web/src/views/Tokens/Tokens.tsx @@ -3,8 +3,9 @@ import { fullId } from "@umami/core"; import { useCurrentAccount, useGetAccountAllTokens } from "@umami/state"; import { Token } from "./Token"; -import { EmptyMessage, VerifyMessage } from "../../components/EmptyMessage"; -import { useIsAccountVerified } from "../../components/Onboarding/useIsAccountVerified"; +import { EmptyMessage } from "../../components/EmptyMessage"; +import { useIsAccountVerified } from "../../components/Onboarding/VerificationFlow/useIsAccountVerified"; +import { VerifyMessage } from "../../components/Onboarding/VerificationFlow/VerifyMessage"; import { ViewOverlay } from "../../components/ViewOverlay/ViewOverlay"; export const Tokens = () => { diff --git a/packages/state/src/reducer.ts b/packages/state/src/reducer.ts index 716c76ee2d..c1e376bcf4 100644 --- a/packages/state/src/reducer.ts +++ b/packages/state/src/reducer.ts @@ -29,6 +29,7 @@ const accountsPersistConfig = { version: VERSION, storage, migrate: createAsyncMigrate(accountsMigrations, { debug: false }), + blacklist: ["password"], }; const rootReducers = combineReducers({ diff --git a/packages/state/src/slices/accounts/State.ts b/packages/state/src/slices/accounts/State.ts index 427edb7dc5..bd971b7ac9 100644 --- a/packages/state/src/slices/accounts/State.ts +++ b/packages/state/src/slices/accounts/State.ts @@ -8,4 +8,5 @@ export type AccountsState = { seedPhrases: Record; secretKeys: Record; current?: RawPkh | undefined; + password?: string | undefined; }; diff --git a/packages/state/src/slices/accounts/accounts.ts b/packages/state/src/slices/accounts/accounts.ts index 0550e6b38a..7445f68f6e 100644 --- a/packages/state/src/slices/accounts/accounts.ts +++ b/packages/state/src/slices/accounts/accounts.ts @@ -18,6 +18,7 @@ export const accountsInitialState: AccountsState = { items: [], seedPhrases: {}, secretKeys: {}, + password: "", }; /** @@ -139,6 +140,9 @@ export const accountsSlice = createSlice({ account.isVerified = isVerified; }, + setPassword: (state, { payload }: { payload: string }) => { + state.password = payload; + }, }, });