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..0a128bba25 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(true);
+ 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}
+
+
+ ))}
+
+
+ : }
+ onClick={() => setIsHidden(!isHidden)}
+ variant="outline"
+ >
+ {isHidden ? "Show" : "Hide"} seed phrase
+
+
+
+ }
+ onClick={handleCopy}
+ variant="outline"
+ >
+ Copy to clipboard
+
+
+
+
+
+
+ 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..98f1a80ff3 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(true);
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
-
+
+
+ }
+ onClick={() => setIsHidden(!isHidden)}
+ variant="ghost"
+ >
+ {isHidden ? "Show" : "Hide"} seed phrase
+
+
+
+ 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({