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

Password change #1675

Merged
merged 15 commits into from Sep 28, 2023
55 changes: 55 additions & 0 deletions playwright/tests/toolbar.spec.ts
@@ -0,0 +1,55 @@
import { test, expect } from '@playwright/test'
import { password } from '../utils/test-inputs'
import { fillPrivateKeyAndPassword } from '../utils/fillPrivateKey'
import { warnSlowApi } from '../utils/warnSlowApi'
import { mockApi } from '../utils/mockApi'

test.beforeEach(async ({ page }) => {
await warnSlowApi(page)
await mockApi(page, 500000000000)
})

const tempPassword = '123'

test.describe('Profile tab', () => {
test('should update password', async ({ page }) => {
await page.goto('/open-wallet/private-key')
await fillPrivateKeyAndPassword(page)
await page.getByTestId('account-selector').click()
await page.getByTestId('toolbar-profile-tab').click()
// use wrong password
await page.getByPlaceholder('Current password').fill('wrongPassword')
await page.getByPlaceholder('New password', { exact: true }).fill(tempPassword)
await page.getByPlaceholder('Re-enter new password').fill(tempPassword)
await page.keyboard.press('Enter')
await expect(page.getByText('Wrong password')).toBeVisible()
// set temp password
await page.getByPlaceholder('Current password').fill(password)
await page.keyboard.press('Enter')
await expect(page.getByText('Password updated.')).toBeVisible()

await page.getByTestId('close-settings-modal').click()
await page.getByRole('button', { name: /Lock profile/ }).click()
await page.getByPlaceholder('Enter your password here').fill(tempPassword)
await page.getByRole('button', { name: /Unlock/ }).click()
await expect(page.getByText('Loading', { exact: true })).toBeVisible()
await expect(page.getByText('Loading', { exact: true })).toBeHidden()

// set back default password
await page.getByTestId('account-selector').click()
await page.getByTestId('toolbar-profile-tab').click()
await page.getByPlaceholder('Current password').fill(tempPassword)
await page.getByPlaceholder('New password', { exact: true }).fill(password)
await page.getByPlaceholder('Re-enter new password').fill(password)
await page.keyboard.press('Enter')
await expect(page.getByText('Password updated.')).toBeVisible()

// validate default password
await page.getByTestId('close-settings-modal').click()
await page.getByRole('button', { name: /Lock profile/ }).click()
await page.getByPlaceholder('Enter your password here').fill(password)
await page.getByRole('button', { name: /Unlock/ }).click()
await expect(page.getByText('Loading', { exact: true })).toBeVisible()
await expect(page.getByText('Loading', { exact: true })).toBeHidden()
})
})
46 changes: 6 additions & 40 deletions src/app/components/Persist/ChoosePasswordFields.tsx
@@ -1,24 +1,13 @@
import { PasswordField } from 'app/components/PasswordField'
import { selectIsPersistenceUnsupported } from 'app/state/persist/selectors'
import { selectUnlockedStatus } from 'app/state/selectUnlockedStatus'
import { Box } from 'grommet/es6/components/Box'
import { CheckBox } from 'grommet/es6/components/CheckBox'
import { FormField } from 'grommet/es6/components/FormField'
import { Paragraph } from 'grommet/es6/components/Paragraph'
import React, { useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'

export interface FormValue {
password1?: string
/**
* Undefined if:
* - persistence is unsupported
* - or is already persisting (unlocked) or skipped unlocking
* - or didn't opt to start persisting
*/
password2?: string
}
import { ChoosePasswordInputFields } from './ChoosePasswordInputFields'

export function ChoosePasswordFields() {
const { t } = useTranslation()
Expand Down Expand Up @@ -65,33 +54,10 @@ export function ChoosePasswordFields() {
{t('persist.createProfile.choosePassword', 'Choose a password')}
</label>
</Paragraph>

<PasswordField<FormValue>
placeholder={t('persist.loginToProfile.enterPasswordHere', 'Enter your password here')}
inputElementId="password1"
name="password1"
validate={value =>
value ? undefined : t('persist.loginToProfile.enterPasswordHere', 'Enter your password here')
}
required
showTip={t('persist.loginToProfile.showPassword', 'Show password')}
hideTip={t('persist.loginToProfile.hidePassword', 'Hide password')}
width="medium"
></PasswordField>

<PasswordField<FormValue>
placeholder={t('persist.createProfile.repeatPassword', 'Re-enter your password')}
inputElementId="password2"
name="password2"
validate={(value, form) =>
form.password1 !== form.password2
? t('persist.createProfile.passwordMismatch', 'Entered password does not match')
: undefined
}
showTip={t('persist.loginToProfile.showPassword', 'Show password')}
hideTip={t('persist.loginToProfile.hidePassword', 'Hide password')}
width="medium"
></PasswordField>
<ChoosePasswordInputFields
password1Placeholder={t('persist.loginToProfile.enterPasswordHere', 'Enter your password here')}
password2Placeholder={t('persist.createProfile.repeatPassword', 'Re-enter your password')}
/>
</>
)}
</Box>
Expand Down
56 changes: 56 additions & 0 deletions src/app/components/Persist/ChoosePasswordInputFields.tsx
@@ -0,0 +1,56 @@
import { PasswordField } from 'app/components/PasswordField'
import { useTranslation } from 'react-i18next'

export interface FormValue {
password1?: string
/**
* Undefined if:
* - persistence is unsupported
* - or is already persisting (unlocked) or skipped unlocking
* - or didn't opt to start persisting
*/
password2?: string
}

interface ChoosePasswordInputFieldsProps {
password1Placeholder?: string
password2Placeholder?: string
}

export function ChoosePasswordInputFields({
password1Placeholder,
password2Placeholder,
}: ChoosePasswordInputFieldsProps) {
const { t } = useTranslation()

return (
<>
<PasswordField<FormValue>
placeholder={password1Placeholder}
inputElementId="password1"
name="password1"
validate={value =>
value ? undefined : t('persist.loginToProfile.enterPasswordHere', 'Enter your password here')
}
required
showTip={t('persist.loginToProfile.showPassword', 'Show password')}
hideTip={t('persist.loginToProfile.hidePassword', 'Hide password')}
width="medium"
/>

<PasswordField<FormValue>
placeholder={password2Placeholder}
inputElementId="password2"
name="password2"
validate={(value, form) =>
form.password1 !== form.password2
? t('persist.createProfile.passwordMismatch', 'Entered password does not match')
: undefined
}
showTip={t('persist.loginToProfile.showPassword', 'Show password')}
hideTip={t('persist.loginToProfile.hidePassword', 'Hide password')}
width="medium"
/>
</>
)
}
Expand Up @@ -9,7 +9,7 @@ import { contactsActions } from 'app/state/contacts'
import { Contact } from 'app/state/contacts/types'
import { ResponsiveLayer } from '../../../ResponsiveLayer'
import { ContactAccountForm } from './ContactAccountForm'
import { layerOverlayMinHeight } from './layer'
import { layerOverlayMinHeight } from '../layer'

interface AddContactProps {
setLayerVisibility: (isVisible: boolean) => void
Expand Down
Expand Up @@ -10,7 +10,7 @@ import { Contact } from 'app/state/contacts/types'
import { Account } from '../Account/Account'
import { ResponsiveLayer } from '../../../ResponsiveLayer'
import { ContactAccountForm } from './ContactAccountForm'
import { layerOverlayMinHeight } from './layer'
import { layerOverlayMinHeight } from '../layer'

interface ContactAccountProps {
contact: Contact
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/Toolbar/Features/Contacts/index.tsx
Expand Up @@ -10,7 +10,7 @@ import { selectContactsList } from 'app/state/contacts/selectors'
import { selectUnlockedStatus } from 'app/state/selectUnlockedStatus'
import { ContactAccount } from './ContactAccount'
import { AddContact } from './AddContact'
import { layerScrollableAreaHeight } from './layer'
import { layerScrollableAreaHeight } from '../layer'

type ContactsListEmptyStateProps = {
children: ReactNode
Expand Down
101 changes: 101 additions & 0 deletions src/app/components/Toolbar/Features/Profile/UpdatePassword.tsx
@@ -0,0 +1,101 @@
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { Box } from 'grommet/es6/components/Box'
import { Button } from 'grommet/es6/components/Button'
import { Form } from 'grommet/es6/components/Form'
import { Paragraph } from 'grommet/es6/components/Paragraph'
import { Notification } from 'grommet/es6/components/Notification'
import {
ChoosePasswordInputFields,
FormValue as ChoosePasswordFieldsFormValue,
} from 'app/components/Persist/ChoosePasswordInputFields'
import { PasswordField } from 'app/components/PasswordField'
import { preventSavingInputsToUserData } from 'app/lib/preventSavingInputsToUserData'
import { persistActions } from 'app/state/persist'
import { selectEnteredWrongPassword, selectLoading } from 'app/state/persist/selectors'

interface FormValue extends ChoosePasswordFieldsFormValue {
currentPassword?: string
}

const defaultFormValue: FormValue = {
currentPassword: '',
password1: '',
password2: '',
}

export const UpdatePassword = () => {
const { t } = useTranslation()
const dispatch = useDispatch()
const [notificationVisible, setNotificationVisible] = useState(false)
const enteredWrongPassword = useSelector(selectEnteredWrongPassword)
const isProfileReloadingAfterPasswordUpdate = useSelector(selectLoading)
const [value, setValue] = useState(defaultFormValue)
const onSubmit = ({ value }: { value: FormValue }) => {
if (!value.currentPassword || !value.password1) {
return
}
dispatch(
persistActions.updatePasswordAsync({
currentPassword: value.currentPassword,
password: value.password1,
}),
)
}

useEffect(() => {
return () => {
dispatch(persistActions.resetWrongPassword())
}
}, [dispatch])

useEffect(() => {
// reloading occurs after successful password update
if (isProfileReloadingAfterPasswordUpdate) {
setNotificationVisible(true)
setValue(defaultFormValue)
}
}, [isProfileReloadingAfterPasswordUpdate])

return (
<Form<FormValue>
onSubmit={onSubmit}
{...preventSavingInputsToUserData}
onChange={nextValue => setValue(nextValue)}
value={value}
>
<Paragraph>
<label htmlFor="password1">{t('toolbar.profile.password.title', 'Set a new password')}</label>
</Paragraph>
<PasswordField<FormValue>
placeholder={t('toolbar.profile.password.current', 'Current password')}
inputElementId="currentPassword"
name="currentPassword"
validate={value =>
value ? undefined : t('toolbar.profile.password.enterCurrent', 'Enter your current password')
}
error={enteredWrongPassword ? t('persist.loginToProfile.wrongPassword', 'Wrong password') : false}
required
showTip={t('persist.loginToProfile.showPassword', 'Show password')}
hideTip={t('persist.loginToProfile.hidePassword', 'Hide password')}
width="medium"
/>
<ChoosePasswordInputFields
password1Placeholder={t('toolbar.profile.password.enterNewPassword', 'New password')}
password2Placeholder={t('toolbar.profile.password.reenterNewPassword', 'Re-enter new password')}
/>
<Box direction="row" justify="end" margin={{ top: 'medium' }}>
<Button primary type="submit" label={t('toolbar.profile.password.submit', 'Update password')} />
</Box>
{notificationVisible && (
<Notification
toast
status={'normal'}
title={t('toolbar.profile.password.success', 'Password updated.')}
onClose={() => setNotificationVisible(false)}
/>
)}
</Form>
)
}
@@ -0,0 +1,51 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Provider } from 'react-redux'
import { configureAppStore } from 'store/configureStore'
import { ThemeProvider } from 'styles/theme/ThemeProvider'
import { persistActions } from 'app/state/persist'
import { UpdatePassword } from '../UpdatePassword'

const renderComponent = (store: any) =>
render(
<Provider store={store}>
<ThemeProvider>
<UpdatePassword />
</ThemeProvider>
</Provider>,
)

describe('<UpdatePassword />', () => {
let store: ReturnType<typeof configureAppStore>

beforeEach(() => {
store = configureAppStore()
})

it('should dispatch action on submit', async () => {
const spy = jest.spyOn(store, 'dispatch')
renderComponent(store)

await userEvent.type(screen.getByPlaceholderText('toolbar.profile.password.current'), 'asd')
await userEvent.type(screen.getByPlaceholderText('toolbar.profile.password.enterNewPassword'), '123')
await userEvent.type(screen.getByPlaceholderText('toolbar.profile.password.reenterNewPassword'), '123')
await userEvent.click(screen.getByRole('button', { name: 'toolbar.profile.password.submit' }))
expect(spy).toHaveBeenCalledWith({
payload: {
currentPassword: 'asd',
password: '123',
},
type: persistActions.updatePasswordAsync.type,
})
})

it('should clear redux password error', () => {
const spy = jest.spyOn(store, 'dispatch')
const { unmount } = renderComponent(store)

unmount()
expect(spy).toHaveBeenCalledWith({
type: persistActions.resetWrongPassword.type,
})
})
})