diff --git a/.changelog/1752.feature.md b/.changelog/1752.feature.md new file mode 100644 index 0000000000..d3b051164c --- /dev/null +++ b/.changelog/1752.feature.md @@ -0,0 +1 @@ +Add remove account feature diff --git a/playwright/tests/toolbar.spec.ts b/playwright/tests/toolbar.spec.ts index 1fad82e1db..d8e574a571 100644 --- a/playwright/tests/toolbar.spec.ts +++ b/playwright/tests/toolbar.spec.ts @@ -1,5 +1,5 @@ -import { test, expect } from '@playwright/test' -import { password, privateKey, privateKeyAddress } from '../utils/test-inputs' +import { test, expect, Page } from '@playwright/test' +import { mnemonic, mnemonicAddress0, password, privateKey, privateKeyAddress } from '../utils/test-inputs' import { fillPrivateKeyAndPassword } from '../utils/fillPrivateKey' import { warnSlowApi } from '../utils/warnSlowApi' import { mockApi } from '../utils/mockApi' @@ -75,4 +75,49 @@ test.describe('My Accounts tab', () => { await page.getByText('I understand, reveal my private key').click() await expect(page.getByText(privateKey)).toBeVisible() }) + + test('should not be able to remove an account', async ({ page }) => { + await page.goto('/open-wallet/private-key') + await fillPrivateKeyAndPassword(page) + await page.getByTestId('account-selector').click() + await page.getByText('Manage').click() + await expect(page.getByText('Delete Account')).toBeDisabled() + }) + + async function openAccountSelectorWithMultipleItems(page: Page) { + await page.goto('/open-wallet/mnemonic') + await page.getByPlaceholder('Enter your keyphrase here').fill(mnemonic) + await page.getByRole('button', { name: /Import my wallet/ }).click() + const uncheckedAccounts = page.getByRole('checkbox', { name: /oasis1/, checked: false }) + await expect(uncheckedAccounts).toHaveCount(3) + for (const account of await uncheckedAccounts.elementHandles()) await account.click() + await page.getByRole('button', { name: /Open/ }).click() + await page.getByTestId('account-selector').click() + await expect(page.getByTestId('account-choice')).toHaveCount(4) + } + + test('should remove currently selected account and switch to the first one in account list', async ({ + page, + }) => { + await openAccountSelectorWithMultipleItems(page) + await page.getByText('Manage').nth(0).click() + await page.getByText('Delete Account').click() + await page.getByRole('textbox').fill('foo') + await page.getByRole('button', { name: 'Yes, delete' }).click() + expect(page.getByText("Type 'delete'")).toBeVisible() + await page.getByRole('textbox').fill('delete') + await page.getByRole('button', { name: 'Yes, delete' }).click() + await expect(page).not.toHaveURL(new RegExp(`/account/${mnemonicAddress0}`)) + await expect(page.getByTestId('account-choice')).toHaveCount(3) + }) + + test('should remove not currently selected account', async ({ page }) => { + await openAccountSelectorWithMultipleItems(page) + await page.getByText('Manage').nth(1).click() + await page.getByText('Delete Account').click() + await page.getByRole('textbox').fill('delete') + await page.getByRole('button', { name: 'Yes, delete' }).click() + await expect(page).toHaveURL(new RegExp(`/account/${mnemonicAddress0}`)) + await expect(page.getByTestId('account-choice')).toHaveCount(3) + }) }) diff --git a/src/app/components/DeleteInputForm/index.tsx b/src/app/components/DeleteInputForm/index.tsx new file mode 100644 index 0000000000..e815ac786f --- /dev/null +++ b/src/app/components/DeleteInputForm/index.tsx @@ -0,0 +1,40 @@ +import { ReactNode } from 'react' +import { Box } from 'grommet/es6/components/Box' +import { Button } from 'grommet/es6/components/Button' +import { useTranslation } from 'react-i18next' +import { TextInput } from 'grommet/es6/components/TextInput' +import { Form } from 'grommet/es6/components/Form' +import { FormField } from 'grommet/es6/components/FormField' + +interface DeleteInputFormProps { + children: ReactNode + onCancel: () => void + onConfirm: () => void +} + +export function DeleteInputForm({ children, onCancel, onConfirm }: DeleteInputFormProps) { + const { t } = useTranslation() + + return ( +
+ ) +} diff --git a/src/app/components/Persist/DeleteProfileButton.tsx b/src/app/components/Persist/DeleteProfileButton.tsx index f0c877129c..092c7b205d 100644 --- a/src/app/components/Persist/DeleteProfileButton.tsx +++ b/src/app/components/Persist/DeleteProfileButton.tsx @@ -1,4 +1,3 @@ -import { Box } from 'grommet/es6/components/Box' import { Button } from 'grommet/es6/components/Button' import { persistActions } from 'app/state/persist' import { useState } from 'react' @@ -7,9 +6,7 @@ import { Trans, useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { Paragraph } from 'grommet/es6/components/Paragraph' import { LoginModalLayout } from './LoginModalLayout' -import { TextInput } from 'grommet/es6/components/TextInput' -import { Form } from 'grommet/es6/components/Form' -import { FormField } from 'grommet/es6/components/FormField' +import { DeleteInputForm } from '../../components/DeleteInputForm' interface DeleteProfileButtonProps { prominent?: boolean @@ -45,7 +42,7 @@ export function DeleteProfileButton({ prominent }: DeleteProfileButtonProps) { onClickOutside={onCancel} onEsc={onCancel} > - + )} > diff --git a/src/app/components/Toolbar/Features/Account/Account.tsx b/src/app/components/Toolbar/Features/Account/Account.tsx index bbfa1af7ba..5f4cb1e68b 100644 --- a/src/app/components/Toolbar/Features/Account/Account.tsx +++ b/src/app/components/Toolbar/Features/Account/Account.tsx @@ -15,7 +15,7 @@ import { DerivationFormatter, DerivationFormatterProps } from './DerivationForma export interface AccountProps { address: string balance: BalanceDetails | undefined - onClick: (address: string) => void + onClick?: (address: string) => void path?: number[] isActive: boolean displayBalance: boolean @@ -48,9 +48,7 @@ export const Account = memo((props: AccountProps) => { fill="horizontal" role="checkbox" aria-checked={props.isActive} - onClick={() => { - props.onClick(props.address) - }} + onClick={props.onClick ? () => props.onClick!(props.address) : undefined} hoverIndicator={{ background: 'brand' }} direction="row" > diff --git a/src/app/components/Toolbar/Features/Account/DeleteAccount.tsx b/src/app/components/Toolbar/Features/Account/DeleteAccount.tsx new file mode 100644 index 0000000000..edc77ee9ee --- /dev/null +++ b/src/app/components/Toolbar/Features/Account/DeleteAccount.tsx @@ -0,0 +1,61 @@ +import { useContext } from 'react' +import { useTranslation } from 'react-i18next' +import { Box } from 'grommet/es6/components/Box' +import { Paragraph } from 'grommet/es6/components/Paragraph' +import { ResponsiveContext } from 'grommet/es6/contexts/ResponsiveContext' +import { Text } from 'grommet/es6/components/Text' +import { ResponsiveLayer } from '../../../ResponsiveLayer' +import { DeleteInputForm } from '../../../../components/DeleteInputForm' +import { Account } from '../Account/Account' +import { Wallet } from '../../../../state/wallet/types' + +interface DeleteAccountProps { + onDelete: () => void + onCancel: () => void + wallet: Wallet +} + +export const DeleteAccount = ({ onCancel, onDelete, wallet }: DeleteAccountProps) => { + const { t } = useTranslation() + const isMobile = useContext(ResponsiveContext) === 'small' + + return ( +