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);
}}
>
- } onClick={copy} {...buttonProps} />
+ } onClick={copy} {...buttonProps}>
+ {content}
+
;
}
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 */}
+ }
+ onClick={saveToFile}
+ >
+ {tStr("buttonSave")}
+
+ >}
+
+ 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 @@
+
>;
}