diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ffcaaf..7561189 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ }, "cSpell.words": [ "Transpiler", + "apos", "arraybuffer", "esnext", "firstseen", diff --git a/src/app/MasterPasswordDialog.tsx b/src/app/MasterPasswordDialog.tsx deleted file mode 100644 index 53cf7e6..0000000 --- a/src/app/MasterPasswordDialog.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { Component } from "react"; - -import Modal from "react-bootstrap/Modal"; -import Button from "react-bootstrap/Button"; -import Form from "react-bootstrap/Form"; - -import { Formik, FormikHelpers } from "formik"; - -import { WalletManager } from "./WalletManager"; - -interface MasterPasswordDialogProps { - hasMasterPassword: boolean; - walletManager: WalletManager; -} - -interface FormValues { - password: string; -} - -export class MasterPasswordDialog extends Component { - async onSubmit({ password }: FormValues, helpers: FormikHelpers): Promise { - try { - if (typeof password !== "string" || password.length === 0) - throw new Error("Password is required."); - - const { hasMasterPassword, walletManager } = this.props; - - if (hasMasterPassword) // Attempt login - await walletManager.testMasterPassword(password); - else // Setup a new master password - await walletManager.setMasterPassword(password); - } catch (e) { // Catch any errors (usually 'invalid password') and display - helpers.setSubmitting(false); - helpers.setErrors({ password: e.message || "Unknown error." }); - console.error(e); - } - } - - browseAsGuest(): void { - this.props.walletManager.browseAsGuest(); - } - - render(): JSX.Element { - const { hasMasterPassword } = this.props; - const body = hasMasterPassword - ?

Enter your master password to access your wallets, or browse - KristWeb as a guest.

- : <> -

Enter a master password to encrypt your wallets, - or browse KristWeb as a guest.

-

- Never forget this password. If you forget it, you will have to - create a new one and add all your wallets again. -

- ; - - return ( - /* TODO: Animation is disabled for now, because react-bootstrap (or more - specifically, react-transition-group) has an incompatibility with - strict mode. */ - - - {({ handleSubmit, handleChange, values, errors, isSubmitting }) => ( -
- - Master password - - - {/* Embed the body text, which depends on whether or not this is - the first time setting up a master password. */} - {body} - - {/* Provide a username field for browser autofill */} - - - {/* Left side, "Browse as guest" button */} - - - {/* Right side */} - {hasMasterPassword - ? <> {/* They have a master password, show login */} - - - - : - } - -
- )} -
-
- ); - } -} diff --git a/src/app/WalletManagerView.tsx b/src/app/WalletManagerView.tsx index 4ffcef3..4aa0b00 100644 --- a/src/app/WalletManagerView.tsx +++ b/src/app/WalletManagerView.tsx @@ -1,6 +1,6 @@ import React, { Component } from "react"; -import { MasterPasswordDialog } from "./MasterPasswordDialog"; +import { MasterPasswordDialog } from "../layouts/dialogs/MasterPasswordDialog"; import { WalletManager } from "./WalletManager"; interface Props { diff --git a/src/layouts/dialogs/HelpWalletStorageDialog.tsx b/src/layouts/dialogs/HelpWalletStorageDialog.tsx new file mode 100644 index 0000000..37822f9 --- /dev/null +++ b/src/layouts/dialogs/HelpWalletStorageDialog.tsx @@ -0,0 +1,56 @@ +import React, { useState, MouseEvent } from "react"; + +import { ModalDialog } from "./ModalDialog"; + +export const HelpWalletStorageLink: React.FC = () => { + const [show, setShow] = useState(false); + + // Help dialog show/close state is essentially handled by the link + const handleClose = () => setShow(false); + const handleShow = (e: MouseEvent) => { + e.preventDefault(); + setShow(true); + }; + + return ( + <> + {/* Add a link to show the dialog */} + (learn more) + + + ); +}; + +type Props = { + show: boolean; + handleClose: () => void; +} + +export const HelpWalletStorageDialog: React.FC = ({ show, handleClose }: Props) => ( + +

+ When you add a wallet to KristWeb, the privatekey for the wallet is + saved to your browser's local storage and encrypted with your + master password. +

+

+ Every wallet you save is encrypted using the same master password, and + you will need to enter it every time you open KristWeb. Your actual + Krist wallet is not modified in any way. +

+

+ When browsing KristWeb as a guest, you do not need to enter a master + password, but it also means that you will not be able to add or use any + wallets. You will still be able to explore the Krist network. +

+
+); diff --git a/src/layouts/dialogs/MasterPasswordDialog.tsx b/src/layouts/dialogs/MasterPasswordDialog.tsx new file mode 100644 index 0000000..2cfe6ab --- /dev/null +++ b/src/layouts/dialogs/MasterPasswordDialog.tsx @@ -0,0 +1,119 @@ +import React, { Component } from "react"; + +import Modal from "react-bootstrap/Modal"; +import Button from "react-bootstrap/Button"; +import Form from "react-bootstrap/Form"; + +import { Formik, FormikHelpers } from "formik"; + +import { HelpWalletStorageLink } from "./HelpWalletStorageDialog"; + +import { WalletManager } from "../../app/WalletManager"; + +interface MasterPasswordDialogProps { + hasMasterPassword: boolean; + walletManager: WalletManager; +} + +interface FormValues { + password: string; +} + +export class MasterPasswordDialog extends Component { + async onSubmit({ password }: FormValues, helpers: FormikHelpers): Promise { + try { + if (typeof password !== "string" || password.length === 0) + throw new Error("Password is required."); + + const { hasMasterPassword, walletManager } = this.props; + + if (hasMasterPassword) // Attempt login + await walletManager.testMasterPassword(password); + else // Setup a new master password + await walletManager.setMasterPassword(password); + } catch (e) { // Catch any errors (usually 'invalid password') and display + helpers.setSubmitting(false); + helpers.setErrors({ password: e.message || "Unknown error." }); + console.error(e); + } + } + + browseAsGuest(): void { + this.props.walletManager.browseAsGuest(); + } + + render(): JSX.Element { + const { hasMasterPassword } = this.props; + const body = hasMasterPassword + ?

Enter your master password to access your wallets, or browse + KristWeb as a guest.

+ : <> +

+ Enter a master password to encrypt your wallets, or browse KristWeb + as a guest . +

+

+ Never forget this password. If you forget it, you will have to + create a new one and add all your wallets again. +

+ ; + + return ( + + + {({ handleSubmit, handleChange, values, errors, isSubmitting }) => ( +
+ + Master password + + + {/* Embed the body text, which depends on whether or not this is + the first time setting up a master password. */} + {body} + + {/* Provide a username field for browser autofill */} + + + {/* Left side, "Browse as guest" button */} + + + {/* Right side */} + {hasMasterPassword + ? <> {/* They have a master password, show login */} + + + + : + } + +
+ )} +
+
+ ); + } +} diff --git a/src/layouts/dialogs/ModalDialog.tsx b/src/layouts/dialogs/ModalDialog.tsx new file mode 100644 index 0000000..55c821f --- /dev/null +++ b/src/layouts/dialogs/ModalDialog.tsx @@ -0,0 +1,48 @@ +import React, { useState, PropsWithChildren, ReactNode } from "react"; + +import Modal from "react-bootstrap/Modal"; +import { CloseButton } from "./utils/CloseButton"; + +import { noop } from "@utils"; + +type Props = { + show: boolean; + + title: string; + + handleClose?: () => void; + hasCloseButton?: boolean; + hasFooterCloseButton?: boolean; + + buttons?: ReactNode; +} + +export const ModalDialog: React.FC = (props: PropsWithChildren) => { + if ((props.hasCloseButton || props.hasFooterCloseButton) && !props.handleClose) + throw new Error("ModalDialog has close button but no close handler"); + + return ( + /* TODO: Animation is disabled for now, because react-bootstrap (or more + specifically, react-transition-group) has an incompatibility with + strict mode. */ + + + {props.title} + + + {/* Custom modal body */} + {props.children} + + + {/* Display the footer close button if we were asked to */} + {props.hasFooterCloseButton && } + + {/* Display the custom buttons if provided */} + {props.buttons} + + + ); +}; diff --git a/src/layouts/dialogs/utils/CloseButton.tsx b/src/layouts/dialogs/utils/CloseButton.tsx new file mode 100644 index 0000000..38b4a2f --- /dev/null +++ b/src/layouts/dialogs/utils/CloseButton.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +import Button from "react-bootstrap/Button"; + +type Props = { + handleClose: () => void; + alignRight?: boolean; +} + +export const CloseButton: React.FC = ({ handleClose, alignRight }: Props) => ( + +); diff --git a/src/layouts/main/components/nav/ConnectionIndicator.tsx b/src/layouts/main/components/nav/ConnectionIndicator.tsx index 15ef25c..2b996a0 100644 --- a/src/layouts/main/components/nav/ConnectionIndicator.tsx +++ b/src/layouts/main/components/nav/ConnectionIndicator.tsx @@ -4,6 +4,6 @@ export const ConnectionIndicator = (): JSX.Element => (
- Connected + Online
); diff --git a/src/utils/index.ts b/src/utils/index.ts index a429b51..5559f9e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -8,3 +8,6 @@ export const fromHex = (input: string): Uint8Array => new Uint8Array((input.match(/.{1,2}/g) || []).map(b => parseInt(b, 16))); + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const noop = (): void => {};