diff --git a/src/components/CopyInputButton.tsx b/src/components/CopyInputButton.tsx index 313cee0..f4b4617 100644 --- a/src/components/CopyInputButton.tsx +++ b/src/components/CopyInputButton.tsx @@ -10,16 +10,23 @@ type Props = ButtonProps & { targetInput: React.RefObject; refocusButton?: boolean; + content?: React.ReactNode; } -export function CopyInputButton({ targetInput, refocusButton, ...buttonProps }: Props): JSX.Element { +export function CopyInputButton({ + targetInput, + refocusButton, + content, + ...buttonProps +}: Props): JSX.Element { const { t } = useTranslation(); const [showCopied, setShowCopied] = useState(false); function copy(e: React.MouseEvent) { if (!targetInput.current) return; - targetInput.current.select(); + // targetInput.current.select(); + targetInput.current.focus({ cursor: "all" }); document.execCommand("copy"); if (refocusButton === undefined || refocusButton) { @@ -36,6 +43,8 @@ if (!visible && showCopied) setShowCopied(false); }} > - ; } diff --git a/src/pages/backup/ExportBackupModal.tsx b/src/pages/backup/ExportBackupModal.tsx new file mode 100644 index 0000000..900208c --- /dev/null +++ b/src/pages/backup/ExportBackupModal.tsx @@ -0,0 +1,115 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under GPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import { useState, useEffect, useRef, Dispatch, SetStateAction } from "react"; +import { Modal, Button, Input } from "antd"; +import { DownloadOutlined } from "@ant-design/icons"; + +import { Trans } from "react-i18next"; +import { useTFns } from "@utils/i18n"; + +import { useWallets } from "@wallets"; + +import { backupExport } from "./backupExport"; +import { CopyInputButton } from "@comp/CopyInputButton"; + +import dayjs from "dayjs"; +import { saveAs } from "file-saver"; + +interface Props { + visible?: boolean; + setVisible: Dispatch>; +} + +export function ExportBackupModal({ + visible, + setVisible +}: Props): JSX.Element { + const { t, tStr, tKey } = useTFns("export."); + + // The generated export code + const [code, setCode] = useState(""); + const inputRef = useRef(null); + + // Used to auto-refresh the code if the wallets change + const { wallets } = useWallets(); + + // Generate the backup code + useEffect(() => { + // Don't bother generating if the modal isn't visible + if (!visible) { + setCode(""); + return; + } + + backupExport() + .then(setCode) + .catch(console.error); + }, [visible, wallets]); + + function saveToFile() { + const blob = new Blob([code], { type: "text/plain;charset=utf-8" }); + saveAs(blob, `KristWeb2-export-${dayjs().format("YYYY-MM-DD--HH-mm-ss")}.txt`); + closeModal(); + } + + function closeModal() { + setVisible(false); + setCode(""); + } + + // Shows a formatted size for the backup code + function Size() { + return {(code.length / 1024).toFixed(1)} KiB; + } + + return + {/* Close button */} + + + {/* Copy to clipboard button */} + + + {/* Save to file button */} + + } + + onCancel={closeModal} + destroyOnClose + > + {/* Description paragraph */} +

+ This secret code contains your wallets and address book contacts. You can + use it to import them in another browser, or to back them up. You will + still need your master password to import the wallets in the future. + Do not share this code with anyone. +

+ + {/* Size calculation */} +

+ Size: +

+ + +
; +} diff --git a/src/pages/backup/backupExport.ts b/src/pages/backup/backupExport.ts new file mode 100644 index 0000000..08ccf57 --- /dev/null +++ b/src/pages/backup/backupExport.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under GPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import { store } from "@app"; + +export async function backupExport(): Promise { + const { salt, tester } = store.getState().masterPassword; + const { wallets } = store.getState().wallets; + + // Get the wallets, skipping those with dontSave set to true + const finalWallets = Object.fromEntries(Object.entries(wallets) + .filter(([_, w]) => w.dontSave !== true)); + + const backup = { + version: "2", + + // Store these to verify the master password is correct when importing + salt, tester, + + wallets: finalWallets, + friends: {} // TODO + }; + + // Convert to base64'd JSON + const code = window.btoa(JSON.stringify(backup)); + return code; +} diff --git a/src/pages/wallets/ManageBackupsDropdown.tsx b/src/pages/wallets/ManageBackupsDropdown.tsx index c6b1169..ff07a10 100644 --- a/src/pages/wallets/ManageBackupsDropdown.tsx +++ b/src/pages/wallets/ManageBackupsDropdown.tsx @@ -10,8 +10,10 @@ import { useTFns } from "@utils/i18n"; import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; +import { useMasterPassword } from "@wallets"; import { ImportBackupModal } from "../backup/ImportBackupModal"; +import { ExportBackupModal } from "../backup/ExportBackupModal"; export function ManageBackupsDropdown(): JSX.Element { const { tStr } = useTFns("myWallets."); @@ -19,6 +21,10 @@ const [importVisible, setImportVisible] = useState(false); const [exportVisible, setExportVisible] = useState(false); + // Used to disable the export button if a master password hasn't been set up + const { hasMasterPassword, salt, tester } = useMasterPassword(); + const allowExport = !!hasMasterPassword && !!salt && !!tester; + return <> @@ -30,7 +36,11 @@ {/* Export backup button */} - + setExportVisible(true)} + > {tStr("exportBackup")} @@ -41,5 +51,6 @@ + ; }