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

Redesign deleting profile #1641

Merged
merged 5 commits into from
Sep 7, 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
21 changes: 13 additions & 8 deletions playwright/tests/persist.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,23 +210,28 @@ test.describe('Persist', () => {
await expect(page.getByTestId('fatalerror-stacktrace')).toBeHidden()
})

test.describe('Opening a wallet after erasing a profile should NOT crash', () => {
test('erasing newly created', async ({ page }) => {
test.describe('Opening a wallet after deleting a profile should NOT crash', () => {
test('deleting newly created', async ({ page }) => {
await page.goto('/open-wallet/private-key')
await fillPrivateKeyAndPassword(page)
await page.getByRole('button', { name: /Lock profile/ }).click()
await testErasingAndCreatingNew(page)
await testDeletingAndCreatingNew(page)
})

test('erasing stored', async ({ page }) => {
test('deleting stored', async ({ page }) => {
await addPersistedStorage(page)
await page.goto('/')
await testErasingAndCreatingNew(page)
await testDeletingAndCreatingNew(page)
})

async function testErasingAndCreatingNew(page: Page) {
page.once('dialog', dialog => dialog.accept())
await page.getByRole('button', { name: /Erase profile/ }).click()
async function testDeletingAndCreatingNew(page: Page) {
await page.getByRole('button', { name: 'Delete profile' }).click()
await page
.getByLabel(
"Are you sure you want to delete this profile? This action cannot be undone and will erase your private keys.To continue please enter 'delete' below.",
)
.fill('delete')
await page.getByRole('button', { name: 'Yes, delete' }).click()

await page.getByRole('button', { name: /Open wallet/ }).click()
await page.getByRole('button', { name: /Private key/ }).click()
Expand Down
85 changes: 85 additions & 0 deletions src/app/components/Persist/DeleteProfileButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Box } from 'grommet/es6/components/Box'
import { Button } from 'grommet/es6/components/Button'
import { persistActions } from 'app/state/persist'
import { useState } from 'react'
import { useDispatch } from 'react-redux'
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'

export function DeleteProfileButton() {
const { t } = useTranslation()
const dispatch = useDispatch()
const navigate = useNavigate()
const [layerVisibility, setLayerVisibility] = useState(false)

const onCancel = () => {
setLayerVisibility(false)
}

const onConfirm = () => {
navigate('/')
dispatch(persistActions.deleteProfileAsync())
}

return (
<>
<Button
label={t('persist.loginToProfile.deleteProfile.button', 'Delete profile')}
onClick={() => setLayerVisibility(true)}
plain
/>
{layerVisibility && (
<LoginModalLayout
title={t('persist.loginToProfile.deleteProfile.title', 'Delete Profile')}
onClickOutside={onCancel}
onEsc={onCancel}
>
<Form onSubmit={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',
),
}}
/>
</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>
</LoginModalLayout>
)}
</>
)
}
23 changes: 23 additions & 0 deletions src/app/components/Persist/LoginModalLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Box } from 'grommet/es6/components/Box'
import { Layer } from 'grommet/es6/components/Layer'
import React from 'react'
import { Header } from 'app/components/Header'

export function LoginModalLayout(props: {
title: string
children: React.ReactNode
onClickOutside?: () => void
onEsc?: () => void
}) {
return (
<Layer modal background="background-front" onClickOutside={props.onClickOutside} onEsc={props.onEsc}>
<Box pad="medium">
<Header level={2} textAlign="center" margin={{ top: 'medium' }}>
{props.title}
</Header>

{props.children}
</Box>
</Layer>
)
}
93 changes: 40 additions & 53 deletions src/app/components/Persist/UnlockForm.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Box } from 'grommet/es6/components/Box'
import { Button } from 'grommet/es6/components/Button'
import { Form } from 'grommet/es6/components/Form'
import { Layer } from 'grommet/es6/components/Layer'
import { Paragraph } from 'grommet/es6/components/Paragraph'
import { persistActions } from 'app/state/persist'
import { selectEnteredWrongPassword } from 'app/state/persist/selectors'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { PasswordField } from 'app/components/PasswordField'
import { Header } from 'app/components/Header'
import { preventSavingInputsToUserData } from 'app/lib/preventSavingInputsToUserData'
import { useNavigate } from 'react-router-dom'
import { DeleteProfileButton } from './DeleteProfileButton'
import { LoginModalLayout } from './LoginModalLayout'

export function UnlockForm() {
const { t } = useTranslation()
Expand All @@ -23,60 +23,47 @@ export function UnlockForm() {
const onSubmit = () => dispatch(persistActions.unlockAsync({ password: password }))

return (
<Layer modal background="background-front">
<Box pad="medium" gap="medium" direction="row" align="center" responsive={false}>
<Form onSubmit={onSubmit} {...preventSavingInputsToUserData}>
<Header>{t('persist.loginToProfile.title', 'Welcome Back!')}</Header>
<Paragraph>
<label htmlFor="password">
{t(
'persist.loginToProfile.description',
'Log into your existing user profile on this computer to access the wallets you already added.',
)}
</label>
</Paragraph>
<LoginModalLayout title={t('persist.loginToProfile.title', 'Welcome Back!')}>
<Form onSubmit={onSubmit} {...preventSavingInputsToUserData}>
<Paragraph>
<label htmlFor="password">
{t(
'persist.loginToProfile.description',
'Log into your existing user profile on this computer to access the wallets you already added.',
)}
</label>
</Paragraph>

<PasswordField
placeholder={t('persist.loginToProfile.enterPasswordHere', 'Enter your password here')}
name="password"
inputElementId="password"
autoFocus
value={password}
onChange={event => setPassword(event.target.value)}
error={enteredWrongPassword ? t('persist.loginToProfile.wrongPassword', 'Wrong password') : false}
showTip={t('persist.loginToProfile.showPassword', 'Show password')}
hideTip={t('persist.loginToProfile.hidePassword', 'Hide password')}
width="auto"
></PasswordField>
<PasswordField
placeholder={t('persist.loginToProfile.enterPasswordHere', 'Enter your password here')}
name="password"
inputElementId="password"
autoFocus
value={password}
onChange={event => setPassword(event.target.value)}
error={enteredWrongPassword ? t('persist.loginToProfile.wrongPassword', 'Wrong password') : false}
showTip={t('persist.loginToProfile.showPassword', 'Show password')}
hideTip={t('persist.loginToProfile.hidePassword', 'Hide password')}
width="auto"
></PasswordField>

<Box direction="row-responsive" gap="medium" justify="between" margin={{ top: 'medium' }}>
<Button type="submit" label={t('persist.loginToProfile.unlock', 'Unlock')} primary />
<Box direction="row-responsive" gap="medium" justify="between" margin={{ top: 'medium' }}>
<Button type="submit" label={t('persist.loginToProfile.unlock', 'Unlock')} primary />

<Button
label={t('persist.loginToProfile.skipUnlocking', 'Continue without the profile')}
onClick={() => {
navigate('/')
dispatch(persistActions.skipUnlocking())
}}
plain
/>
</Box>

<Box direction="row" margin={{ top: 'large' }}>
<Button
label={t('persist.loginToProfile.eraseProfile', 'Erase profile')}
onClick={() => {
// TODO: improve UX
if (window.confirm('Are you sure?')) {
navigate('/')
dispatch(persistActions.eraseAsync())
}
}}
plain
/>
</Box>
</Form>
<Button
label={t('persist.loginToProfile.skipUnlocking', 'Continue without the profile')}
onClick={() => {
navigate('/')
dispatch(persistActions.skipUnlocking())
}}
plain
/>
</Box>
</Form>
<Box direction="row" margin={{ top: 'large' }}>
{/* Must be outside the Form otherwise submit button in DeleteProfileButton submits parent Form too */}
<DeleteProfileButton />
</Box>
</Layer>
</LoginModalLayout>
)
}
2 changes: 1 addition & 1 deletion src/app/state/persist/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const persistSlice = createSlice({
/**
* Remove encrypted state from localStorage and reload.
*/
eraseAsync(state) {
deleteProfileAsync(state) {
state.loading = true
},
},
Expand Down
6 changes: 3 additions & 3 deletions src/app/state/persist/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ function* handleAsyncPersistActions(action: AnyAction) {
yield* call(unlockAsync, action)
} else if (persistActions.lockAsync.match(action)) {
yield* call(lockAsync, action)
} else if (persistActions.eraseAsync.match(action)) {
yield* call(eraseAsync, action)
} else if (persistActions.deleteProfileAsync.match(action)) {
yield* call(deleteProfileAsync, action)
} else if (persistActions.setUnlockedRootState.match(action)) {
// Skip encrypting the same state
} else if (persistActions.resetRootState.match(action)) {
Expand Down Expand Up @@ -94,7 +94,7 @@ function* lockAsync(action: ReturnType<typeof persistActions.lockAsync>) {
// Implies state.loading = false
}

function* eraseAsync(action: ReturnType<typeof persistActions.eraseAsync>) {
function* deleteProfileAsync(action: ReturnType<typeof persistActions.deleteProfileAsync>) {
yield* call([window.localStorage, window.localStorage.removeItem], STORAGE_FIELD)
yield* put(persistActions.resetRootState())
// Implies state.loading = false
Expand Down
9 changes: 8 additions & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,16 @@
},
"loading": "Loading",
"loginToProfile": {
"deleteProfile": {
"button": "Delete profile",
"confirm": "Yes, delete",
"confirmationKeyword": "delete",
"confirmationKeywordInvalid": "Type 'delete'",
"description": "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.",
"title": "Delete Profile"
},
"description": "Log into your existing user profile on this computer to access the wallets you already added.",
"enterPasswordHere": "Enter your password here",
"eraseProfile": "Erase profile",
"hidePassword": "Hide password",
"showPassword": "Show password",
"skipUnlocking": "Continue without the profile",
Expand Down