From d3ee58cbb5ba4ff596d480db641d924c407b5628 Mon Sep 17 00:00:00 2001 From: Oleg Chendighelean Date: Mon, 9 Dec 2024 17:07:07 +0000 Subject: [PATCH] Add reveal mnemonic logic --- apps/desktop/public/electron.js | 10 +- apps/desktop/public/preload.js | 3 + .../components/Onboarding/notice/Notice.tsx | 2 +- .../showSeedphrase/ShowSeedphrase.tsx | 169 +++++++++++++----- apps/desktop/src/global.d.ts | 13 ++ .../src/components/CopyButton/CopyButton.tsx | 24 ++- .../components/MnemonicWord/MnemonicWord.tsx | 7 +- .../RecordSeedphraseModal.tsx | 44 +++-- .../MnemonicAutocomplete.tsx | 4 +- .../state/src/hooks/useAsyncActionHandler.ts | 1 - 10 files changed, 210 insertions(+), 67 deletions(-) create mode 100644 apps/desktop/src/global.d.ts diff --git a/apps/desktop/public/electron.js b/apps/desktop/public/electron.js index 57f4db9c70..51a6ff25bf 100644 --- a/apps/desktop/public/electron.js +++ b/apps/desktop/public/electron.js @@ -1,5 +1,5 @@ // Module to control the application lifecycle and the native browser window. -const { app, BrowserWindow, shell, net, ipcMain, protocol } = require("electron"); +const { app, BrowserWindow, shell, net, ipcMain, protocol, clipboard } = require("electron"); const path = require("path"); const url = require("url"); const process = require("process"); @@ -242,6 +242,14 @@ function start() { // Listen to install-app-update event from UI, start update on getting the event. ipcMain.on("install-app-update", () => autoUpdater.quitAndInstall()); + + ipcMain.on("clipboard-write", (_, text) => { + clipboard.writeText(text); + }); + + ipcMain.on("clipboard-clear", () => { + clipboard.clear(); + }); } start(); diff --git a/apps/desktop/public/preload.js b/apps/desktop/public/preload.js index 2d6d934eb4..d9a69b6cfc 100644 --- a/apps/desktop/public/preload.js +++ b/apps/desktop/public/preload.js @@ -17,4 +17,7 @@ contextBridge.exposeInMainWorld("electronAPI", { // Notify Electron that app update should be installed. installAppUpdateAndQuit: () => ipcRenderer.send("install-app-update"), + + clipboardWriteText: text => ipcRenderer.send("clipboard-write", text), + clipboardClear: () => ipcRenderer.send("clipboard-clear"), }); diff --git a/apps/desktop/src/components/Onboarding/notice/Notice.tsx b/apps/desktop/src/components/Onboarding/notice/Notice.tsx index fd6353aeda..b4a2910537 100644 --- a/apps/desktop/src/components/Onboarding/notice/Notice.tsx +++ b/apps/desktop/src/components/Onboarding/notice/Notice.tsx @@ -14,7 +14,7 @@ export const Notice = ({ goToStep }: { goToStep: (step: OnboardingStep) => void content: "Make sure there is no one around you or looking over your shoulder.", }, { - content: "Do not copy and paste the Seed Phrase or store it on your device.", + content: "Do not store Seed Phrase on your device.", }, { content: "Do not take a screenshot of your Seed Phrase.", diff --git a/apps/desktop/src/components/Onboarding/showSeedphrase/ShowSeedphrase.tsx b/apps/desktop/src/components/Onboarding/showSeedphrase/ShowSeedphrase.tsx index 75a19c37e0..dc6f2b4c1e 100644 --- a/apps/desktop/src/components/Onboarding/showSeedphrase/ShowSeedphrase.tsx +++ b/apps/desktop/src/components/Onboarding/showSeedphrase/ShowSeedphrase.tsx @@ -1,59 +1,134 @@ -import { Button, Flex, Heading, SimpleGrid, Text, VStack } from "@chakra-ui/react"; +import { + Button, + Flex, + Heading, + Popover, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, + SimpleGrid, + Text, + VStack, + useDisclosure, +} from "@chakra-ui/react"; +import { useState } from "react"; -import { KeyIcon } from "../../../assets/icons"; +import { EyeIcon, EyeSlashIcon, FileCopyIcon, KeyIcon } from "../../../assets/icons"; import colors from "../../../style/colors"; import { ModalContentWrapper } from "../ModalContentWrapper"; import { type OnboardingStep, type ShowSeedphraseStep } from "../OnboardingStep"; +const COPY_TIMEOUT = 30_000; +const COPIED_POPUP_DURATION = 2000; + export const ShowSeedphrase = ({ goToStep, account, }: { goToStep: (step: OnboardingStep) => void; account: ShowSeedphraseStep["account"]; -}) => ( - } - subtitle="Please record the following 24 words in sequence in order to restore it in the future." - title="Record Seed Phrase" - > - - - {account.mnemonic.split(" ").map((item, index) => ( - - { + const [isHidden, setIsHidden] = useState(false); + const { + isOpen: isPopoverOpen, + onOpen: setIsPopoverOpen, + onClose: setIsPopoverClose, + } = useDisclosure(); + + const handleCopy = () => { + window.electronAPI.clipboardWriteText(account.mnemonic); + setIsPopoverOpen(); + + setTimeout(() => { + setIsPopoverClose(); + }, COPIED_POPUP_DURATION); + + setTimeout(() => { + window.electronAPI.clipboardClear(); + }, COPY_TIMEOUT); + }; + + return ( + } + subtitle="Please record the following 24 words in sequence in order to restore it in the future." + title="Record Seed Phrase" + > + + + {account.mnemonic.split(" ").map((item, index) => ( + - {index + 1} - - - {item} - - - ))} - - - - -); + + {index + 1} + + + {isHidden ? "********" : item} + + + ))} + + + + + + + + + + + + Copied! + + + + + + + + + ); +}; diff --git a/apps/desktop/src/global.d.ts b/apps/desktop/src/global.d.ts new file mode 100644 index 0000000000..409c3cc1b5 --- /dev/null +++ b/apps/desktop/src/global.d.ts @@ -0,0 +1,13 @@ +export {}; + +declare global { + interface Window { + electronAPI: { + clipboardWriteText: (text: string) => void; + clipboardClear: () => void; + onDeeplink: (callback: (url: string) => void) => void; + onAppUpdateDownloaded: (callback: () => void) => void; + installAppUpdateAndQuit: () => void; + }; + } +} diff --git a/apps/web/src/components/CopyButton/CopyButton.tsx b/apps/web/src/components/CopyButton/CopyButton.tsx index 44f65f4caa..94616fad55 100644 --- a/apps/web/src/components/CopyButton/CopyButton.tsx +++ b/apps/web/src/components/CopyButton/CopyButton.tsx @@ -13,12 +13,21 @@ import { type MouseEvent, type PropsWithChildren } from "react"; import { useColor } from "../../styles/useColor"; +const COPY_TIMEOUT = 30_000; + +type CopyButtonProps = { + value: string; + isCopyDisabled?: boolean; + isDisposable?: boolean; +} & ButtonProps; + export const CopyButton = ({ value, children, isCopyDisabled = false, + isDisposable = false, ...props -}: PropsWithChildren<{ value: string; isCopyDisabled?: boolean } & ButtonProps>) => { +}: PropsWithChildren) => { const color = useColor(); const { isOpen, onOpen, onClose } = useDisclosure(); @@ -30,7 +39,18 @@ export const CopyButton = ({ event.stopPropagation(); setTimeout(onClose, 1000); - return navigator.clipboard.writeText(value); + + return navigator.clipboard.writeText(value).then(() => { + if (isDisposable) { + setTimeout(() => { + try { + void navigator.clipboard.writeText(""); + } catch (error: unknown) { + console.error("Failed to clear clipboard", error); + } + }, COPY_TIMEOUT); + } + }); }; return ( diff --git a/apps/web/src/components/MnemonicWord/MnemonicWord.tsx b/apps/web/src/components/MnemonicWord/MnemonicWord.tsx index f0549b98d8..9f96a0514d 100644 --- a/apps/web/src/components/MnemonicWord/MnemonicWord.tsx +++ b/apps/web/src/components/MnemonicWord/MnemonicWord.tsx @@ -7,6 +7,7 @@ import { useColor } from "../../styles/useColor"; type MnemonicWordProps = { index: number; word?: string; + isHidden?: boolean; indexProps?: TextProps; autocompleteProps?: ComponentProps; } & GridItemProps; @@ -14,6 +15,7 @@ type MnemonicWordProps = { export const MnemonicWord = ({ index, word, + isHidden, autocompleteProps, indexProps, ...props @@ -39,11 +41,14 @@ export const MnemonicWord = ({ {autocompleteProps && } {word && ( - {word} + {isHidden ? "*******" : word} )} diff --git a/apps/web/src/components/Onboarding/VerificationFlow/RecordSeedphraseModal.tsx b/apps/web/src/components/Onboarding/VerificationFlow/RecordSeedphraseModal.tsx index 6a58937362..e5c0367ae4 100644 --- a/apps/web/src/components/Onboarding/VerificationFlow/RecordSeedphraseModal.tsx +++ b/apps/web/src/components/Onboarding/VerificationFlow/RecordSeedphraseModal.tsx @@ -12,9 +12,10 @@ import { Text, } from "@chakra-ui/react"; import { useDynamicModalContext } from "@umami/components"; +import { useState } from "react"; import { VerifySeedphraseModal } from "./VerifySeedphraseModal"; -import { CopyIcon, KeyIcon } from "../../../assets/icons"; +import { CopyIcon, EyeIcon, EyeOffIcon, KeyIcon } from "../../../assets/icons"; import { useColor } from "../../../styles/useColor"; import { ModalBackButton } from "../../BackButton"; import { ModalCloseButton } from "../../CloseButton"; @@ -29,6 +30,7 @@ export const RecordSeedphraseModal = ({ seedPhrase }: CopySeedphraseModalProps) const color = useColor(); const { openWith } = useDynamicModalContext(); const words = seedPhrase.split(" "); + const [isHidden, setIsHidden] = useState(false); return ( @@ -49,6 +51,7 @@ export const RecordSeedphraseModal = ({ seedPhrase }: CopySeedphraseModalProps) gridRowGap={{ base: "12px", md: "18px" }} gridColumnGap={{ base: "8px", md: "12px" }} gridTemplateColumns={{ base: "repeat(3, 1fr)", md: "repeat(4, 1fr)" }} + userSelect="none" > {words.map((word, index) => ( ))} - - - Copy - + + + + + Copy + + diff --git a/packages/state/src/hooks/useAsyncActionHandler.ts b/packages/state/src/hooks/useAsyncActionHandler.ts index f6e8f4584d..e450349c3b 100644 --- a/packages/state/src/hooks/useAsyncActionHandler.ts +++ b/packages/state/src/hooks/useAsyncActionHandler.ts @@ -38,7 +38,6 @@ export const useAsyncActionHandler = () => { try { return await fn(); } catch (error: any) { - console.log("error", error); const errorContext = getErrorContext(error); toast({