From fd935367f1adc6e982d600725ffba7ad22d66947 Mon Sep 17 00:00:00 2001 From: Oleg Chendighelean Date: Wed, 11 Sep 2024 14:42:52 +0100 Subject: [PATCH 1/2] Add remove account group button --- .../AccountSelectorModal.tsx | 46 ++++++++++++- .../ConfirmationModal/ConfirmationModal.tsx | 64 +++++++++++++++++++ .../src/components/ConfirmationModal/index.ts | 1 + apps/web/src/components/Menu/LogoutModal.tsx | 53 +++------------ 4 files changed, 118 insertions(+), 46 deletions(-) create mode 100644 apps/web/src/components/ConfirmationModal/ConfirmationModal.tsx create mode 100644 apps/web/src/components/ConfirmationModal/index.ts diff --git a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx index e291ec02f0..e5522d8b6e 100644 --- a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx +++ b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx @@ -13,8 +13,19 @@ import { VStack, } from "@chakra-ui/react"; import { useDynamicModalContext } from "@umami/components"; -import { type ImplicitAccount, type MnemonicAccount, getAccountGroupLabel } from "@umami/core"; -import { accountsActions, useGetAccountBalance, useImplicitAccounts } from "@umami/state"; +import { + type Account, + type ImplicitAccount, + type MnemonicAccount, + getAccountGroupLabel, +} from "@umami/core"; +import { + accountsActions, + useGetAccountBalance, + useImplicitAccounts, + useRemoveMnemonic, + useRemoveNonMnemonic, +} from "@umami/state"; import { prettyTezAmount } from "@umami/tezos"; import { groupBy } from "lodash"; import { useDispatch } from "react-redux"; @@ -25,6 +36,7 @@ import { useColor } from "../../styles/useColor"; import { AccountTile } from "../AccountTile"; import { ModalCloseButton } from "../CloseButton"; import { DeriveMnemonicAccountModal } from "./DeriveMnemonicAccountModal"; +import { ConfirmationModal } from "../ConfirmationModal"; import { OnboardOptionsModal } from "../Onboarding/OnboardOptions"; import { useIsAccountVerified } from "../Onboarding/VerificationFlow"; @@ -33,6 +45,8 @@ export const AccountSelectorModal = () => { const color = useColor(); const getBalance = useGetAccountBalance(); const isVerified = useIsAccountVerified(); + const removeMnemonic = useRemoveMnemonic(); + const removeNonMnemonic = useRemoveNonMnemonic(); const { openWith, onClose } = useDynamicModalContext(); const dispatch = useDispatch(); @@ -52,6 +66,33 @@ export const AccountSelectorModal = () => { } }; + const buttonLabel = (isLast: boolean) => (isLast ? "Remove & Off-board" : "Remove"); + const description = (isLast: boolean, type: string) => + isLast + ? "Removing all your accounts will off-board you from Umami. This will remove or reset all customized settings to their defaults. Personal data (including saved contacts, password and accounts) won't be affected." + : `Are you sure you want to remove all of your ${type}?`; + + const onRemove = (accounts: Account[]) => { + const account = accounts[0]; + const isLast = accounts.length === accounts.length; + + return openWith( + { + if (account.type === "mnemonic") { + removeMnemonic(account.seedFingerPrint); + } else if (account.type !== "multisig") { + removeNonMnemonic(account.type); + } + onClose(); + }} + title="Remove All Accounts" + /> + ); + }; + return ( @@ -81,6 +122,7 @@ export const AccountSelectorModal = () => { color={color("500")} aria-label={`Remove ${type} accounts`} icon={} + onClick={() => onRemove(accounts)} size="sm" variant="ghost" /> diff --git a/apps/web/src/components/ConfirmationModal/ConfirmationModal.tsx b/apps/web/src/components/ConfirmationModal/ConfirmationModal.tsx new file mode 100644 index 0000000000..880732ef82 --- /dev/null +++ b/apps/web/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -0,0 +1,64 @@ +import { + Button, + Center, + Heading, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Text, +} from "@chakra-ui/react"; +import { useDynamicModalContext } from "@umami/components"; + +import { useColor } from "../../styles/useColor"; +import { ModalBackButton } from "../BackButton"; +import { ModalCloseButton } from "../CloseButton"; + +export const ConfirmationModal = ({ + title, + description, + buttonLabel, + onSubmit, +}: { + title: string; + buttonLabel: string; + description?: string; + onSubmit: () => void; +}) => { + const { onClose } = useDynamicModalContext(); + const onClick = () => { + onSubmit(); + onClose(); + }; + + const color = useColor(); + + return ( + + + {title} + + + + +
+ + {description} + +
+
+ + + +
+ ); +}; diff --git a/apps/web/src/components/ConfirmationModal/index.ts b/apps/web/src/components/ConfirmationModal/index.ts new file mode 100644 index 0000000000..13a5a10e7d --- /dev/null +++ b/apps/web/src/components/ConfirmationModal/index.ts @@ -0,0 +1 @@ +export { ConfirmationModal } from "./ConfirmationModal"; diff --git a/apps/web/src/components/Menu/LogoutModal.tsx b/apps/web/src/components/Menu/LogoutModal.tsx index 816f8101c4..9d14bf38fa 100644 --- a/apps/web/src/components/Menu/LogoutModal.tsx +++ b/apps/web/src/components/Menu/LogoutModal.tsx @@ -1,48 +1,13 @@ -import { - Button, - Center, - Heading, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, - Text, -} from "@chakra-ui/react"; import { logout } from "@umami/state"; -import { useColor } from "../../styles/useColor"; import { persistor } from "../../utils/persistor"; -import { ModalCloseButton } from "../CloseButton"; +import { ConfirmationModal } from "../ConfirmationModal"; -export const LogoutModal = () => { - const color = useColor(); - - return ( - - - Logout - - - -
- - Before logging out, make sure your mnemonic phrase is securely saved. Losing this phrase - could result in permanent loss of access to your data. - -
-
- - - -
- ); -}; +export const LogoutModal = () => ( + logout(persistor)} + title="Logout" + /> +); From cfecc9755461db3293d589b190d77a5f4682052b Mon Sep 17 00:00:00 2001 From: Oleg Chendighelean Date: Wed, 11 Sep 2024 15:18:31 +0100 Subject: [PATCH 2/2] Add tests --- .../AccountSelectorModal.test.tsx | 89 ++++++++++++++++++- .../AccountSelectorModal.tsx | 16 ++-- .../RemoveAccountModal.tsx | 39 ++------ apps/web/src/components/Menu/LogoutModal.tsx | 2 +- 4 files changed, 104 insertions(+), 42 deletions(-) diff --git a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.test.tsx b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.test.tsx index 170ecf69bc..27fcd37fd6 100644 --- a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.test.tsx +++ b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.test.tsx @@ -4,7 +4,13 @@ import { mockMnemonicAccount, mockSocialAccount, } from "@umami/core"; -import { type UmamiStore, accountsActions, addTestAccounts, makeStore } from "@umami/state"; +import { + type UmamiStore, + accountsActions, + addTestAccount, + addTestAccounts, + makeStore, +} from "@umami/state"; import { AccountSelectorModal } from "./AccountSelectorModal"; import { DeriveMnemonicAccountModal } from "./DeriveMnemonicAccountModal"; @@ -58,6 +64,87 @@ describe("", () => { } ); + describe("when clicking 'Remove account group' button", () => { + it.each([ + ["mnemonic", mockMnemonicAccount(0)], + ["ledger", mockLedgerAccount(1)], + ["social", mockSocialAccount(2)], + ])( + "opens confirmation modal when clicking remove button for %s accounts", + async (_, account) => { + const user = userEvent.setup(); + await renderInModal(, store); + const accountLabel = getAccountGroupLabel(account); + + const removeButton = screen.getByLabelText(`Remove ${accountLabel} accounts`); + await act(() => user.click(removeButton)); + + expect(screen.getByText("Remove All Accounts")).toBeInTheDocument(); + + await waitFor(() => + expect( + screen.getByText(`Are you sure you want to remove all of your ${accountLabel}?`) + ).toBeVisible() + ); + } + ); + + it("removes mnemonic accounts when confirmed", async () => { + const user = userEvent.setup(); + await renderInModal(, store); + const account = mockMnemonicAccount(0); + const accountLabel = getAccountGroupLabel(account); + const removeButton = screen.getByLabelText(`Remove ${accountLabel} accounts`); + await act(() => user.click(removeButton)); + + const confirmButton = screen.getByText("Remove"); + await act(() => user.click(confirmButton)); + + expect(store.getState().accounts.seedPhrases[account.seedFingerPrint]).toBe(undefined); + expect(store.getState().accounts.items.length).toBe(2); + }); + + it.each([ + ["ledger", mockLedgerAccount(1)], + ["social", mockSocialAccount(2)], + ])("removes %s accounts when confirmed", async (_, account) => { + const user = userEvent.setup(); + await renderInModal(, store); + const accountLabel = getAccountGroupLabel(account); + const accounts = store.getState().accounts.items; + + const removeButton = screen.getByLabelText(`Remove ${accountLabel} accounts`); + await act(() => user.click(removeButton)); + + const confirmButton = screen.getByText("Remove"); + await act(() => user.click(confirmButton)); + + expect(store.getState().accounts.items.length).toBe(accounts.length - 1); + }); + + it('shows "Remove & Off-board" message when removing last group of accounts', async () => { + store.dispatch(accountsActions.reset()); + addTestAccount(store, mockSocialAccount(0)); + + const user = userEvent.setup(); + const accountLabel = getAccountGroupLabel(mockSocialAccount(0)); + await renderInModal(, store); + + const removeButton = screen.getByLabelText(`Remove ${accountLabel} accounts`); + await act(() => user.click(removeButton)); + + expect(screen.getByText("Remove & Off-board")).toBeInTheDocument(); + + await waitFor(() => + expect( + screen.getByText( + "Removing all your accounts will off-board you from Umami. This will remove or reset all customized settings to their defaults. Personal data (including saved contacts, password and accounts) won't be affected." + ) + ).toBeVisible() + ); + }); + }); + it("opens appropriate modal when clicking 'Add Account' button", async () => { const user = userEvent.setup(); const { openWith } = dynamicModalContextMock; diff --git a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx index e5522d8b6e..63e76e01f9 100644 --- a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx +++ b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx @@ -41,17 +41,17 @@ import { OnboardOptionsModal } from "../Onboarding/OnboardOptions"; import { useIsAccountVerified } from "../Onboarding/VerificationFlow"; export const AccountSelectorModal = () => { - const accounts = useImplicitAccounts(); + const implicitAccounts = useImplicitAccounts(); const color = useColor(); const getBalance = useGetAccountBalance(); const isVerified = useIsAccountVerified(); const removeMnemonic = useRemoveMnemonic(); const removeNonMnemonic = useRemoveNonMnemonic(); - const { openWith, onClose } = useDynamicModalContext(); + const { openWith, goBack, onClose } = useDynamicModalContext(); const dispatch = useDispatch(); - const groupedAccounts = groupBy(accounts, getAccountGroupLabel); + const groupedAccounts = groupBy(implicitAccounts, getAccountGroupLabel); const handleDeriveAccount = (account?: ImplicitAccount) => { if (!account) { @@ -72,21 +72,21 @@ export const AccountSelectorModal = () => { ? "Removing all your accounts will off-board you from Umami. This will remove or reset all customized settings to their defaults. Personal data (including saved contacts, password and accounts) won't be affected." : `Are you sure you want to remove all of your ${type}?`; - const onRemove = (accounts: Account[]) => { + const onRemove = (type: string, accounts: Account[]) => { const account = accounts[0]; - const isLast = accounts.length === accounts.length; + const isLast = accounts.length === implicitAccounts.length; return openWith( { if (account.type === "mnemonic") { removeMnemonic(account.seedFingerPrint); } else if (account.type !== "multisig") { removeNonMnemonic(account.type); } - onClose(); + goBack(); }} title="Remove All Accounts" /> @@ -122,7 +122,7 @@ export const AccountSelectorModal = () => { color={color("500")} aria-label={`Remove ${type} accounts`} icon={} - onClick={() => onRemove(accounts)} + onClick={() => onRemove(type, accounts)} size="sm" variant="ghost" /> diff --git a/apps/web/src/components/AccountSelectorModal/RemoveAccountModal.tsx b/apps/web/src/components/AccountSelectorModal/RemoveAccountModal.tsx index 351fe9ffa6..a034e2ae49 100644 --- a/apps/web/src/components/AccountSelectorModal/RemoveAccountModal.tsx +++ b/apps/web/src/components/AccountSelectorModal/RemoveAccountModal.tsx @@ -1,19 +1,8 @@ -import { - Button, - Flex, - Heading, - ModalContent, - ModalFooter, - ModalHeader, - Text, -} from "@chakra-ui/react"; import { useDynamicModalContext } from "@umami/components"; import { type LedgerAccount, type SecretKeyAccount, type SocialAccount } from "@umami/core"; import { useImplicitAccounts, useRemoveAccount } from "@umami/state"; -import { AlertIcon } from "../../assets/icons"; -import { useColor } from "../../styles/useColor"; -import { ModalCloseButton } from "../CloseButton"; +import { ConfirmationModal } from "../ConfirmationModal"; type RemoveAccountModalProps = { account: SocialAccount | LedgerAccount | SecretKeyAccount; @@ -22,7 +11,6 @@ type RemoveAccountModalProps = { export const RemoveAccountModal = ({ account }: RemoveAccountModalProps) => { const { goBack, onClose } = useDynamicModalContext(); const removeAccount = useRemoveAccount(); - const color = useColor(); const isLastImplicitAccount = useImplicitAccounts().length === 1; @@ -50,24 +38,11 @@ export const RemoveAccountModal = ({ account }: RemoveAccountModalProps) => { } return ( - - - - - - Remove Account - - - {description} - - - - - - - - + ); }; diff --git a/apps/web/src/components/Menu/LogoutModal.tsx b/apps/web/src/components/Menu/LogoutModal.tsx index 9d14bf38fa..c6fd81248a 100644 --- a/apps/web/src/components/Menu/LogoutModal.tsx +++ b/apps/web/src/components/Menu/LogoutModal.tsx @@ -6,7 +6,7 @@ import { ConfirmationModal } from "../ConfirmationModal"; export const LogoutModal = () => ( logout(persistor)} title="Logout" />