Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Account #1752

Merged
merged 5 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/1752.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add remove account feature
49 changes: 47 additions & 2 deletions playwright/tests/toolbar.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
})
buberdds marked this conversation as resolved.
Show resolved Hide resolved
})
40 changes: 40 additions & 0 deletions src/app/components/DeleteInputForm/index.tsx
Original file line number Diff line number Diff line change
@@ -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()

Check warning on line 16 in src/app/components/DeleteInputForm/index.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/DeleteInputForm/index.tsx#L16

Added line #L16 was not covered by tests

return (

Check warning on line 18 in src/app/components/DeleteInputForm/index.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/DeleteInputForm/index.tsx#L18

Added line #L18 was not covered by tests
<Form onSubmit={onConfirm}>
{children}
<FormField
name="type_delete"
validate={(value: string | undefined) =>
!value || value.toLowerCase() !== t('deleteForm.confirmationKeyword', 'delete').toLowerCase()
? t('deleteForm.hint', `Type '{{confirmationKeyword}}'`, {

Check warning on line 25 in src/app/components/DeleteInputForm/index.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/DeleteInputForm/index.tsx#L24-L25

Added lines #L24 - L25 were not covered by tests
confirmationKeyword: t('deleteForm.confirmationKeyword', 'delete'),
})
: undefined

Check warning on line 28 in src/app/components/DeleteInputForm/index.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/DeleteInputForm/index.tsx#L28

Added line #L28 was not covered by tests
}
>
<TextInput id="type_delete" name="type_delete" />
</FormField>

<Box direction="row" justify="between" pad={{ top: 'large' }}>
<Button secondary label={t('common.cancel', 'Cancel')} onClick={onCancel} />
<Button type="submit" label={t('deleteForm.confirm', 'Yes, delete')} primary color="status-error" />
</Box>
</Form>
)
}
36 changes: 4 additions & 32 deletions src/app/components/Persist/DeleteProfileButton.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -45,45 +42,20 @@ export function DeleteProfileButton({ prominent }: DeleteProfileButtonProps) {
onClickOutside={onCancel}
onEsc={onCancel}
>
<Form onSubmit={onConfirm}>
<DeleteInputForm onCancel={onCancel} onConfirm={onConfirm}>
<Paragraph>
<label htmlFor="type_delete">
<Trans
t={t}
i18nKey="persist.loginToProfile.deleteProfile.description"
defaults="Are you sure you want to delete this profile? This action cannot be undone and will <strong>erase your private keys</strong>.<br/><br/>To continue please enter '{{confirmationKeyword}}' below."
values={{
confirmationKeyword: t(
'persist.loginToProfile.deleteProfile.confirmationKeyword',
'delete',
),
confirmationKeyword: t('deleteForm.confirmationKeyword', 'delete'),
}}
/>
</label>
</Paragraph>
<FormField
name="type_delete"
validate={(value: string | undefined) =>
!value ||
value.toLowerCase() !==
t('persist.loginToProfile.deleteProfile.confirmationKeyword', 'delete').toLowerCase()
? t('persist.loginToProfile.deleteProfile.confirmationKeywordInvalid', `Type 'delete'`)
buberdds marked this conversation as resolved.
Show resolved Hide resolved
: undefined
}
>
<TextInput id="type_delete" name="type_delete" />
</FormField>

<Box direction="row" justify="between" pad={{ top: 'large' }}>
<Button secondary label={t('common.cancel', 'Cancel')} onClick={onCancel} />
<Button
type="submit"
label={t('persist.loginToProfile.deleteProfile.confirm', 'Yes, delete')}
primary
color="status-error"
/>
</Box>
</Form>
</DeleteInputForm>
</LoginModalLayout>
)}
</>
Expand Down
6 changes: 2 additions & 4 deletions src/app/components/Toolbar/Features/Account/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
>
Expand Down
61 changes: 61 additions & 0 deletions src/app/components/Toolbar/Features/Account/DeleteAccount.tsx
Original file line number Diff line number Diff line change
@@ -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'

Check warning on line 20 in src/app/components/Toolbar/Features/Account/DeleteAccount.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/Toolbar/Features/Account/DeleteAccount.tsx#L19-L20

Added lines #L19 - L20 were not covered by tests

return (

Check warning on line 22 in src/app/components/Toolbar/Features/Account/DeleteAccount.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/Toolbar/Features/Account/DeleteAccount.tsx#L22

Added line #L22 was not covered by tests
<ResponsiveLayer
onClickOutside={onCancel}
onEsc={onCancel}
animation="none"
background="background-front"
modal
margin={isMobile ? 'none' : 'xlarge'}
>
<Box margin="medium">
<Box flex="grow" justify="center">

Check warning on line 32 in src/app/components/Toolbar/Features/Account/DeleteAccount.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/Toolbar/Features/Account/DeleteAccount.tsx#L29-L32

Added lines #L29 - L32 were not covered by tests
<Text weight="bold" size="medium" textAlign="center" margin={{ bottom: 'large' }}>
{t('toolbar.settings.delete.title', 'Delete Account')}
</Text>
<Text size="medium" textAlign="center" margin={{ bottom: 'medium' }}>
{t(
'toolbar.settings.delete.description',
'Are you sure you want to delete the following account?',
)}
</Text>
<Account address={wallet.address} balance={undefined} displayBalance={false} isActive />

<DeleteInputForm onCancel={onCancel} onConfirm={onDelete}>
<label htmlFor="type_delete">
<Paragraph fill textAlign="center">
{t(
'toolbar.settings.delete.inputHelp',
`This action cannot be undone. To continue please enter '{{confirmationKeyword}}' below.`,
{
confirmationKeyword: t('deleteForm.confirmationKeyword', 'delete'),
},
)}
</Paragraph>
</label>
</DeleteInputForm>
</Box>
</Box>
</ResponsiveLayer>
)
}
16 changes: 12 additions & 4 deletions src/app/components/Toolbar/Features/Account/ManageableAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,30 @@ import { ManageableAccountDetails } from './ManageableAccountDetails'
export const ManageableAccount = ({
wallet,
isActive,
onClick,
deleteWallet,
selectWallet,
}: {
wallet: Wallet
isActive: boolean
onClick: (address: string) => void
deleteWallet?: (address: string) => void
selectWallet: (address: string) => void
}) => {
const { t } = useTranslation()
const [layerVisibility, setLayerVisibility] = useState(false)
const isMobile = useContext(ResponsiveContext) === 'small'
const handleDelete = deleteWallet
? (address: string) => {
deleteWallet(address)
setLayerVisibility(false)
}
: undefined

return (
<>
<Account
address={wallet.address}
balance={wallet.balance}
onClick={onClick}
onClick={selectWallet}
isActive={isActive}
path={wallet.path}
displayBalance={true}
Expand All @@ -47,7 +55,7 @@ export const ManageableAccount = ({
height={{ min: isMobile ? 'auto' : layerOverlayMinHeight }}
pad={{ vertical: 'medium' }}
>
<ManageableAccountDetails wallet={wallet} />
<ManageableAccountDetails deleteAccount={handleDelete} wallet={wallet} />
<Box direction="row" justify="between" pad={{ top: 'large' }}>
<Button
secondary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import { Tab } from 'grommet/es6/components/Tab'
import { Tabs } from 'grommet/es6/components/Tabs'
import { Text } from 'grommet/es6/components/Text'
import { Tip } from 'grommet/es6/components/Tip'
import { Copy } from 'grommet-icons/es6/icons/Copy'
import { CircleInformation } from 'grommet-icons/es6/icons/CircleInformation'
import { useTranslation } from 'react-i18next'
import { NoTranslate } from 'app/components/NoTranslate'
import { Wallet } from '../../../../state/wallet/types'
Expand All @@ -16,14 +18,18 @@
import { layerOverlayMinHeight } from '../layer'
import { LayerContainer } from './../LayerContainer'
import { uintToBase64, hex2uint } from '../../../../lib/helpers'
import { DeleteAccount } from './DeleteAccount'

interface ManageableAccountDetailsProps {
/** If undefined: delete button is disabled */
deleteAccount: undefined | ((address: string) => void)
wallet: Wallet
}

export const ManageableAccountDetails = ({ wallet }: ManageableAccountDetailsProps) => {
export const ManageableAccountDetails = ({ deleteAccount, wallet }: ManageableAccountDetailsProps) => {
const { t } = useTranslation()
const [layerVisibility, setLayerVisibility] = useState(false)
const [deleteLayerVisibility, setDeleteLayerVisibility] = useState(false)
const [acknowledge, setAcknowledge] = useState(false)
const [notificationVisible, setNotificationVisible] = useState(false)
const isMobile = useContext(ResponsiveContext) === 'small'
Expand All @@ -45,12 +51,42 @@
<Text size="small" margin={'small'}>
<DerivationFormatter pathDisplay={wallet.pathDisplay} type={wallet.type} />
</Text>
<Button
alignSelf="start"
label={t('toolbar.settings.exportPrivateKey.title', 'Export Private Key')}
disabled={!wallet.privateKey}
onClick={() => setLayerVisibility(true)}
/>
<Box justify="between" direction="row">
<Button
alignSelf="start"
label={t('toolbar.settings.exportPrivateKey.title', 'Export Private Key')}
disabled={!wallet.privateKey}
onClick={() => setLayerVisibility(true)}

Check warning on line 59 in src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx#L59

Added line #L59 was not covered by tests
/>

{deleteAccount ? (
<Button

Check warning on line 63 in src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx#L63

Added line #L63 was not covered by tests
plain
color="status-error"
label={t('toolbar.settings.delete.title', 'Delete Account')}
onClick={() => setDeleteLayerVisibility(true)}

Check warning on line 67 in src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx#L67

Added line #L67 was not covered by tests
/>
) : (
<Tip

Check warning on line 70 in src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx#L70

Added line #L70 was not covered by tests
content={t(
'toolbar.settings.delete.tooltip',
'You must have at least one account at all times.',
)}
dropProps={{ align: { bottom: 'top' } }}
>
<Box>
<Button
icon={<CircleInformation size="18px" color="status-error" />}
disabled={true}
plain
color="status-error"
label={t('toolbar.settings.delete.title', 'Delete Account')}
onClick={() => setDeleteLayerVisibility(true)}

Check warning on line 84 in src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx#L84

Added line #L84 was not covered by tests
/>
</Box>
</Tip>
)}
</Box>
</Box>
{layerVisibility && (
<LayerContainer hideLayer={hideLayer}>
Expand Down Expand Up @@ -107,6 +143,13 @@
</Tabs>
</LayerContainer>
)}
{deleteLayerVisibility && deleteAccount && (
<DeleteAccount
onDelete={() => deleteAccount(wallet.address)}
onCancel={() => setDeleteLayerVisibility(false)}

Check warning on line 149 in src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/components/Toolbar/Features/Account/ManageableAccountDetails.tsx#L146-L149

Added lines #L146 - L149 were not covered by tests
wallet={wallet}
/>
)}
{notificationVisible && (
<Notification
toast
Expand Down
Loading
Loading