Skip to content

Commit

Permalink
Add reveal mnemonic logic
Browse files Browse the repository at this point in the history
  • Loading branch information
OKendigelyan committed Dec 9, 2024
1 parent 8ced9ad commit 0ee882a
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 65 deletions.
10 changes: 9 additions & 1 deletion apps/desktop/public/electron.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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();
3 changes: 3 additions & 0 deletions apps/desktop/public/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
});
2 changes: 1 addition & 1 deletion apps/desktop/src/components/Onboarding/notice/Notice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ describe("<ShowSeedphrase />", () => {
const user = userEvent.setup();
render(fixture());

await act(() => user.click(screen.getByTestId("show-seedphrase-button")));

mnemonic1.split(" ").forEach(word => {
expect(screen.getByText(word)).toBeInTheDocument();
});
Expand Down
170 changes: 123 additions & 47 deletions apps/desktop/src/components/Onboarding/showSeedphrase/ShowSeedphrase.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,135 @@
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"];
}) => (
<ModalContentWrapper
icon={<KeyIcon width="24px" height="24px" />}
subtitle="Please record the following 24 words in sequence in order to restore it in the future."
title="Record Seed Phrase"
>
<VStack>
<SimpleGrid columns={3} spacing={2}>
{account.mnemonic.split(" ").map((item, index) => (
<Flex
key={index}
width="126px"
padding="6px"
border="1px dashed"
borderColor={colors.gray[500]}
borderRadius="4px"
>
<Heading
width="18px"
marginRight="10px"
paddingTop="2px"
color={colors.gray[450]}
textAlign="right"
size="sm"
}) => {
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 (
<ModalContentWrapper
icon={<KeyIcon width="24px" height="24px" />}
subtitle="Please record the following 24 words in sequence in order to restore it in the future."
title="Record Seed Phrase"
>
<VStack>
<SimpleGrid userSelect="none" columns={3} spacing={2}>
{account.mnemonic.split(" ").map((item, index) => (
<Flex
key={index}
width="126px"
padding="6px"
border="1px dashed"
borderColor={colors.gray[500]}
borderRadius="4px"
>
{index + 1}
</Heading>
<Text data-testid={`mnemonic-word-${index}`} size="sm">
{item}
</Text>
</Flex>
))}
</SimpleGrid>
<Button
width="100%"
marginTop="20px"
onClick={_ => {
goToStep({ type: "verifySeedphrase", account });
}}
size="lg"
>
OK, I've recorded it
</Button>
</VStack>
</ModalContentWrapper>
);
<Heading
width="18px"
marginRight="10px"
paddingTop="2px"
color={colors.gray[450]}
textAlign="right"
size="sm"
>
{index + 1}
</Heading>
<Text
sx={{
WebkitTextSecurity: isHidden ? "disc" : "none",
}}
data-testid={`mnemonic-word-${index}`}
size="sm"
>
{isHidden ? "********" : item}
</Text>
</Flex>
))}
</SimpleGrid>
<Flex justifyContent="space-between" gap="16px" width="100%" marginTop="20px">
<Button
width="100%"
data-testid="show-seedphrase-button"
leftIcon={isHidden ? <EyeSlashIcon /> : <EyeIcon />}
onClick={() => setIsHidden(!isHidden)}
variant="outline"
>
{isHidden ? "Show" : "Hide"} seed phrase
</Button>
<Popover autoFocus={false} closeOnBlur={false} isOpen={isPopoverOpen}>
<PopoverTrigger>
<Button
width="100%"
leftIcon={<FileCopyIcon stroke={colors.gray[450]} />}
onClick={handleCopy}
variant="outline"
>
Copy to clipboard
</Button>
</PopoverTrigger>
<PopoverContent maxWidth="max-content" background="white">
<PopoverArrow background="white !important" />
<PopoverBody padding="8px 12px">
<Text color="black" fontWeight="medium" size="sm">
Copied!
</Text>
</PopoverBody>
</PopoverContent>
</Popover>
</Flex>
<Button
width="100%"
marginTop="8px"
onClick={_ => {
goToStep({ type: "verifySeedphrase", account });
}}
size="lg"
>
OK, I've recorded it
</Button>
</VStack>
</ModalContentWrapper>
);
};
13 changes: 13 additions & 0 deletions apps/desktop/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
}
24 changes: 22 additions & 2 deletions apps/web/src/components/CopyButton/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CopyButtonProps>) => {
const color = useColor();

const { isOpen, onOpen, onClose } = useDisclosure();
Expand All @@ -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 (
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/components/MnemonicWord/MnemonicWord.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { useColor } from "../../styles/useColor";
type MnemonicWordProps = {
index: number;
word?: string;
isHidden?: boolean;
indexProps?: TextProps;
autocompleteProps?: ComponentProps<typeof MnemonicAutocomplete>;
} & GridItemProps;

export const MnemonicWord = ({
index,
word,
isHidden,
autocompleteProps,
indexProps,
...props
Expand All @@ -39,11 +41,14 @@ export const MnemonicWord = ({
{autocompleteProps && <MnemonicAutocomplete {...autocompleteProps} />}
{word && (
<Text
sx={{
WebkitTextSecurity: isHidden ? "disc" : "none",
}}
paddingLeft={{ base: "22px", md: "26px" }}
fontSize={{ base: "12px", md: "14px" }}
fontWeight="medium"
>
{word}
{isHidden ? "*******" : word}
</Text>
)}
</GridItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 (
<ModalContent>
Expand All @@ -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) => (
<MnemonicWord
Expand All @@ -66,21 +69,38 @@ export const RecordSeedphraseModal = ({ seedPhrase }: CopySeedphraseModalProps)
borderColor={color("100")}
borderRadius="full"
index={index}
isHidden={isHidden}
word={word}
/>
))}
</Grid>
<CopyButton
gap="8px"
width="full"
marginTop="16px"
fontSize="14px"
value={seedPhrase}
variant="ghost"
>
<Icon as={CopyIcon} boxSize="18px" color={color("400")} />
Copy
</CopyButton>
<Flex gap="16px" width="100%" marginTop="16px">
<Button
gap="4px"
display="flex"
width="full"
fontSize="14px"
fontWeight="400"
leftIcon={
<Icon as={isHidden ? EyeOffIcon : EyeIcon} boxSize="18px" color={color("400")} />
}
onClick={() => setIsHidden(!isHidden)}
variant="ghost"
>
{isHidden ? "Show" : "Hide"} seed phrase
</Button>
<CopyButton
gap="8px"
width="full"
fontSize="14px"
isDisposable
value={seedPhrase}
variant="ghost"
>
<Icon as={CopyIcon} boxSize="18px" color={color("400")} />
Copy
</CopyButton>
</Flex>
</ModalBody>
<ModalFooter>
<Button
Expand Down
Loading

0 comments on commit 0ee882a

Please sign in to comment.