Skip to content

Commit

Permalink
Merge pull request #1675 from oasisprotocol/mz/profile
Browse files Browse the repository at this point in the history
Password change
  • Loading branch information
buberdds authored Sep 28, 2023
2 parents 3e6cd4d + a7fc74d commit 35ed8ab
Show file tree
Hide file tree
Showing 19 changed files with 1,042 additions and 58 deletions.
55 changes: 55 additions & 0 deletions playwright/tests/toolbar.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
/>
</>
)
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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>
)
}
Original file line number Diff line number Diff line change
@@ -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,
})
})
})
Loading

0 comments on commit 35ed8ab

Please sign in to comment.