Skip to content

Commit

Permalink
Merge pull request #1752 from oasisprotocol/mz/accRemove
Browse files Browse the repository at this point in the history
Remove Account
  • Loading branch information
buberdds authored Nov 10, 2023
2 parents d5f94e6 + e6be568 commit baf8166
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 56 deletions.
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)
})
})
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()

return (
<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}}'`, {
confirmationKeyword: t('deleteForm.confirmationKeyword', 'delete'),
})
: 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('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'`)
: 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'

return (
<ResponsiveLayer
onClickOutside={onCancel}
onEsc={onCancel}
animation="none"
background="background-front"
modal
margin={isMobile ? 'none' : 'xlarge'}
>
<Box margin="medium">
<Box flex="grow" justify="center">
<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 { ResponsiveContext } from 'grommet/es6/contexts/ResponsiveContext'
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 { AddressBox } from '../../../AddressBox'
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 @@ export const ManageableAccountDetails = ({ wallet }: ManageableAccountDetailsPro
<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)}
/>

{deleteAccount ? (
<Button
plain
color="status-error"
label={t('toolbar.settings.delete.title', 'Delete Account')}
onClick={() => setDeleteLayerVisibility(true)}
/>
) : (
<Tip
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)}
/>
</Box>
</Tip>
)}
</Box>
</Box>
{layerVisibility && (
<LayerContainer hideLayer={hideLayer}>
Expand Down Expand Up @@ -107,6 +143,13 @@ export const ManageableAccountDetails = ({ wallet }: ManageableAccountDetailsPro
</Tabs>
</LayerContainer>
)}
{deleteLayerVisibility && deleteAccount && (
<DeleteAccount
onDelete={() => deleteAccount(wallet.address)}
onCancel={() => setDeleteLayerVisibility(false)}
wallet={wallet}
/>
)}
{notificationVisible && (
<Notification
toast
Expand Down
Loading

0 comments on commit baf8166

Please sign in to comment.