diff --git a/public/locales/en.json b/public/locales/en.json index 7b23443..8a458d4 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -124,7 +124,8 @@ "popoverAuthoriseButton": "Authorise", "popoverDescription": "Enter your master password to decrypt your wallets.", "popoverDescriptionEncrypt": "Enter your master password to encrypt and decrypt your wallets.", - "forcedAuthWarning": "You were automatically logged in by an insecure debug setting." + "forcedAuthWarning": "You were automatically logged in by an insecure debug setting.", + "earlyAuthError": "The app has not loaded fully yet, please try again." }, "myWallets": { diff --git a/src/components/auth/AuthContext.tsx b/src/components/auth/AuthContext.tsx index 4be3007..ded26d3 100644 --- a/src/components/auth/AuthContext.tsx +++ b/src/components/auth/AuthContext.tsx @@ -8,7 +8,7 @@ import { AuthMasterPasswordModal } from "./AuthMasterPasswordModal"; import { SetMasterPasswordModal } from "./SetMasterPasswordModal"; -type PromptAuthFn = (encrypt: boolean | undefined, onAuthed?: () => void) => void; +export type PromptAuthFn = (encrypt: boolean | undefined, onAuthed?: () => void) => void; export const AuthContext = createContext(undefined); interface ModalProps { @@ -27,10 +27,16 @@ const [modalProps, setModalProps] = useState({ encrypt: false }); const promptAuth: PromptAuthFn = useCallback((encrypt, onAuthed) => { + if (isAuthed) { + // Pass-through auth directly if already authed. + onAuthed?.(); + return; + } + setModalProps({ encrypt, onAuthed }); setModalVisible(true); setClicked(true); - }, []); + }, [isAuthed]); const submit = useCallback(() => { setModalVisible(false); diff --git a/src/components/auth/AuthorisedAction.tsx b/src/components/auth/AuthorisedAction.tsx index 6093508..b445c51 100644 --- a/src/components/auth/AuthorisedAction.tsx +++ b/src/components/auth/AuthorisedAction.tsx @@ -2,11 +2,11 @@ // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import React, { FC, useContext } from "react"; +import { message } from "antd"; -import { useSelector } from "react-redux"; -import { RootState } from "@store"; +import i18n from "@utils/i18n"; -import { AuthContext } from "./AuthContext"; +import { AuthContext, PromptAuthFn } from "./AuthContext"; interface Props { encrypt?: boolean; @@ -15,7 +15,6 @@ } export const AuthorisedAction: FC = ({ encrypt, onAuthed, children }) => { - const isAuthed = useSelector((s: RootState) => s.masterPassword.isAuthed); const promptAuth = useContext(AuthContext); // This is used to pass the 'onClick' prop down to the child. The child MUST @@ -25,17 +24,12 @@ const child = React.Children.only(children) as React.ReactElement; // Wrap the single child element and override onClick - if (isAuthed) { - // Already authed, run onAuthed immediately - return React.cloneElement(child, { onClick: (e: MouseEvent) => { - e.preventDefault(); - onAuthed?.(); - }}); - } else { - // Not authed, prompt for either set password or auth password - return React.cloneElement(child, { onClick: (e: MouseEvent) => { - e.preventDefault(); - promptAuth?.(encrypt, onAuthed); - }}); - } + return React.cloneElement(child, { onClick: (e: MouseEvent) => { + e.preventDefault(); + promptAuth?.(encrypt, onAuthed); + }}); }; + +export const useAuth = (): PromptAuthFn => + useContext(AuthContext) || (() => + message.error(i18n.t("masterPassword.earlyAuthError"))); diff --git a/src/components/auth/index.ts b/src/components/auth/index.ts new file mode 100644 index 0000000..456edee --- /dev/null +++ b/src/components/auth/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under AGPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt + +export * from "./AuthorisedAction"; diff --git a/src/pages/contacts/ContactActions.tsx b/src/pages/contacts/ContactActions.tsx index e29a262..16ae545 100644 --- a/src/pages/contacts/ContactActions.tsx +++ b/src/pages/contacts/ContactActions.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import React, { useCallback, useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { Modal, Dropdown, Menu } from "antd"; import { EditOutlined, DeleteOutlined, ExclamationCircleOutlined, SendOutlined @@ -9,7 +9,7 @@ import { useTFns } from "@utils/i18n"; -import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; +import { useAuth } from "@comp/auth"; import { OpenEditContactFn } from "./ContactEditButton"; import { OpenSendTxFn } from "@comp/transactions/SendTransactionModalLink"; @@ -27,6 +27,7 @@ openSendTx, }: Props): JSX.Element { const { t, tStr } = useTFns("addressBook."); + const promptAuth = useAuth(); const showContactDeleteConfirm = useCallback((): void => { Modal.confirm({ @@ -44,33 +45,15 @@ const memoDropdown = useMemo(() => [ - // Edit contact button - openEditContact(contact)} - > - {React.cloneElement(leftButton as React.ReactElement, { - className: "ant-btn-left" - })} - , - - // Dropdown button - rightButton - ]} - + onClick={() => promptAuth(true, () => openEditContact(contact))} trigger={["click"]} - - overlay={( + overlay={() => ( {/* Send tx button */} - - openSendTx(undefined, contact.address)} - > -
{tStr("actionsSendTransaction")}
-
+ } + onClick={() => promptAuth(false, () => + openSendTx(undefined, contact.address))}> + {tStr("actionsSendTransaction")} @@ -85,7 +68,8 @@ {/* Edit button */} , [ - tStr, contact, showContactDeleteConfirm, openEditContact, openSendTx + tStr, contact, promptAuth, openSendTx, openEditContact, + showContactDeleteConfirm ]); return memoDropdown; diff --git a/src/pages/names/mgmt/NameActions.tsx b/src/pages/names/mgmt/NameActions.tsx index 74da6dc..02a7929 100644 --- a/src/pages/names/mgmt/NameActions.tsx +++ b/src/pages/names/mgmt/NameActions.tsx @@ -5,12 +5,12 @@ import { Button, Dropdown, Menu } from "antd"; import { DownOutlined, SwapOutlined, SendOutlined, EditOutlined } from "@ant-design/icons"; -import { useTranslation } from "react-i18next"; +import { useTFns } from "@utils/i18n"; import { KristName } from "@api/types"; import { useNameSuffix } from "@utils/currency"; -import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; +import { useAuth } from "@comp/auth"; import { OpenEditNameFn } from "./NameEditModalLink"; import { OpenSendTxFn } from "@comp/transactions/SendTransactionModalLink"; @@ -29,73 +29,63 @@ openNameEdit, openSendTx }: Props): JSX.Element { - const { t } = useTranslation(); + const { tStr } = useTFns("names."); + const promptAuth = useAuth(); const nameSuffix = useNameSuffix(); const nameWithSuffix = `${name.name}.${nameSuffix}`; - // The dropdown menu, used if we own the name - const buttonMenu = useMemo(() => isOwn - ? - {/* Transfer Krist button */} - - openSendTx(undefined, nameWithSuffix)} - > -
{t("names.actionsTransferKrist")}
-
-
- - - - {/* Update A record */} - - openNameEdit("update", name.name, name.a)} - > -
{t("names.actionsUpdateARecord")}
-
-
- - {/* Transfer name */} - - openNameEdit("transfer", name.name)} - > -
{t("names.actionsTransferName")}
-
-
-
- : undefined, - [t, isOwn, name.a, name.name, nameWithSuffix, openSendTx, openNameEdit]); - const actions = useMemo(() => isOwn ? ( // Actions dropdown (own name) ( + + {/* Transfer Krist button */} + } + onClick={() => promptAuth(false, () => + openSendTx(undefined, nameWithSuffix))}> + {tStr("actionsTransferKrist")} + + + + + {/* Update A record */} + } + onClick={() => promptAuth(false, () => + openNameEdit("update", name.name, name.a))}> + {tStr("actionsUpdateARecord")} + + + {/* Transfer name */} + } + onClick={() => promptAuth(false, () => + openNameEdit("transfer", name.name))}> + {tStr("actionsTransferName")} + + + )} > ) : ( // Send transaction button (not own name) - openSendTx(undefined, nameWithSuffix)} + - + {tStr("actionsSendKrist")} + ), - [t, buttonMenu, isOwn, nameWithSuffix, openSendTx]); + [tStr, isOwn, nameWithSuffix, openSendTx, name.a, name.name, + openNameEdit, promptAuth]); return actions; } diff --git a/src/pages/wallets/WalletActions.tsx b/src/pages/wallets/WalletActions.tsx index b8f7e97..031dce6 100644 --- a/src/pages/wallets/WalletActions.tsx +++ b/src/pages/wallets/WalletActions.tsx @@ -1,17 +1,16 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import React, { useCallback, useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { Modal, Dropdown, Menu } from "antd"; -import { MenuClickEventHandler } from "rc-menu/lib/interface"; import { EditOutlined, DeleteOutlined, InfoCircleOutlined, ExclamationCircleOutlined, SendOutlined } from "@ant-design/icons"; -import { useTranslation } from "react-i18next"; +import { useTFns } from "@utils/i18n"; -import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; +import { useAuth } from "@comp/auth"; import { OpenEditWalletFn } from "./WalletEditButton"; import { OpenSendTxFn } from "@comp/transactions/SendTransactionModalLink"; import { OpenWalletInfoFn } from "./info/WalletInfoModal"; @@ -31,78 +30,48 @@ openSendTx, openWalletInfo, }: Props): JSX.Element { - const { t } = useTranslation(); + const { t, tStr } = useTFns("myWallets."); + const promptAuth = useAuth(); const showWalletDeleteConfirm = useCallback((): void => { Modal.confirm({ icon: , - title: t("myWallets.actionsDeleteConfirm"), - content: t("myWallets.actionsDeleteConfirmDescription"), + title: tStr("actionsDeleteConfirm"), + content: tStr("actionsDeleteConfirmDescription"), onOk: () => deleteWallet(wallet), okText: t("dialog.yes"), okType: "danger", cancelText: t("dialog.no") }); - }, [t, wallet]); - - const onMenuClick: MenuClickEventHandler = useCallback(e => { - switch (e.key) { - // "Wallet info" button - case "2": - return openWalletInfo(wallet); - - // "Delete wallet" button - case "3": - return showWalletDeleteConfirm(); - } - }, [wallet, openWalletInfo, showWalletDeleteConfirm]); + }, [t, tStr, wallet]); const memoDropdown = useMemo(() => [ - // Edit wallet button - openEditWallet(wallet)} - > - {/* Tooltip was removed for now as an optimisation */} - {/* */} - {React.cloneElement(leftButton as React.ReactElement, { - className: "ant-btn-left", // force the border-radius - disabled: wallet.dontSave - })} - {/* */} - , - - // Dropdown button - rightButton - ]} - + onClick={() => promptAuth(true, () => openEditWallet(wallet))} trigger={["click"]} - - overlay={( - + overlay={() => ( + {/* Send tx button */} - - openSendTx(wallet)}> -
{t("myWallets.actionsSendTransaction")}
-
+ } + onClick={() => promptAuth(false, () => openSendTx(wallet))}> + {tStr("actionsSendTransaction")} {/* Wallet info button */} - - {t("myWallets.actionsWalletInfo")} + } + onClick={() => openWalletInfo(wallet)}> + {tStr("actionsWalletInfo")} {/* Delete button */} - - {t("myWallets.actionsDelete")} + } + onClick={showWalletDeleteConfirm}> + {tStr("actionsDelete")}
)} @@ -110,7 +79,8 @@ {/* Edit button */} , [ - t, wallet, onMenuClick, openEditWallet, openSendTx + tStr, wallet, promptAuth, + openEditWallet, openSendTx, openWalletInfo, showWalletDeleteConfirm ]); return memoDropdown; diff --git a/src/pages/wallets/WalletsPageActions.tsx b/src/pages/wallets/WalletsPageActions.tsx index de33543..9809093 100644 --- a/src/pages/wallets/WalletsPageActions.tsx +++ b/src/pages/wallets/WalletsPageActions.tsx @@ -2,7 +2,7 @@ // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import { useEffect, useState, Dispatch, SetStateAction } from "react"; -import { Button, Menu } from "antd"; +import { Button, Menu, Space } from "antd"; import { PlusOutlined, ImportOutlined, ExportOutlined } from "@ant-design/icons"; import { useTFns } from "@utils/i18n"; @@ -31,7 +31,7 @@ }: ExtraButtonsProps): JSX.Element { const { tStr } = useTFns("myWallets."); - return <> + return {/* Manage backups */} setAddWalletVisible(true)}> - ; + ; } export function useWalletsPageActions(): JSX.Element {