-
-
Notifications
You must be signed in to change notification settings - Fork 478
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): add reset password form (#1964)
add reset password form
- Loading branch information
Showing
8 changed files
with
331 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import api from './api'; | ||
|
||
type Response = { | ||
redirectTo: string; | ||
}; | ||
|
||
export const sendResetPasswordSmsPasscode = async (phone: string) => { | ||
await api | ||
.post('/api/session/reset-password/sms/send-passcode', { | ||
json: { | ||
phone, | ||
}, | ||
}) | ||
.json(); | ||
|
||
return { success: true }; | ||
}; | ||
|
||
export const verifyResetPasswordSmsPasscode = async (phone: string, code: string) => | ||
api | ||
.post('/api/session/reset-password/sms/verify-passcode', { | ||
json: { | ||
phone, | ||
code, | ||
}, | ||
}) | ||
.json<Response>(); | ||
|
||
export const sendResetPasswordEmailPasscode = async (email: string) => { | ||
await api | ||
.post('/api/session/reset-password/email/send-passcode', { | ||
json: { | ||
email, | ||
}, | ||
}) | ||
.json(); | ||
|
||
return { success: true }; | ||
}; | ||
|
||
export const verifyResetPasswordEmailPasscode = async (email: string, code: string) => | ||
api | ||
.post('/api/session/reset-password/email/verify-passcode', { | ||
json: { | ||
email, | ||
code, | ||
}, | ||
}) | ||
.json<Response>(); | ||
|
||
export const resetPassword = async (password: string) => | ||
api | ||
.post('/api/session/reset-password', { | ||
json: { password }, | ||
}) | ||
.json<Response>(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
packages/ui/src/containers/ResetPassword/index.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
@use '@/scss/underscore' as _; | ||
|
||
.form { | ||
@include _.flex-column; | ||
|
||
> * { | ||
width: 100%; | ||
} | ||
|
||
.inputField { | ||
margin-bottom: _.unit(4); | ||
} | ||
} |
112 changes: 112 additions & 0 deletions
112
packages/ui/src/containers/ResetPassword/index.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { fireEvent, act, waitFor } from '@testing-library/react'; | ||
|
||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; | ||
import { resetPassword } from '@/apis/reset-password'; | ||
|
||
import ResetPassword from '.'; | ||
|
||
jest.mock('@/apis/reset-password', () => ({ | ||
resetPassword: jest.fn(async () => ({ redirectTo: '/' })), | ||
})); | ||
|
||
describe('<ResetPassword />', () => { | ||
test('default render', () => { | ||
const { queryByText, container } = renderWithPageContext(<ResetPassword />); | ||
expect(container.querySelector('input[name="new-password"]')).not.toBeNull(); | ||
expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull(); | ||
expect(queryByText('action.confirm')).not.toBeNull(); | ||
}); | ||
|
||
test('password are required', () => { | ||
const { queryByText, getByText } = renderWithPageContext(<ResetPassword />); | ||
const submitButton = getByText('action.confirm'); | ||
fireEvent.click(submitButton); | ||
|
||
expect(queryByText('password_required')).not.toBeNull(); | ||
expect(resetPassword).not.toBeCalled(); | ||
}); | ||
|
||
test('password less than 6 chars should throw', () => { | ||
const { queryByText, getByText, container } = renderWithPageContext(<ResetPassword />); | ||
const submitButton = getByText('action.confirm'); | ||
const passwordInput = container.querySelector('input[name="new-password"]'); | ||
|
||
if (passwordInput) { | ||
fireEvent.change(passwordInput, { target: { value: '12345' } }); | ||
} | ||
|
||
act(() => { | ||
fireEvent.click(submitButton); | ||
}); | ||
|
||
expect(queryByText('password_min_length')).not.toBeNull(); | ||
|
||
expect(resetPassword).not.toBeCalled(); | ||
|
||
act(() => { | ||
// Clear error | ||
if (passwordInput) { | ||
fireEvent.change(passwordInput, { target: { value: '123456' } }); | ||
} | ||
}); | ||
|
||
expect(queryByText('password_min_length')).toBeNull(); | ||
}); | ||
|
||
test('password mismatch with confirmPassword should throw', () => { | ||
const { queryByText, getByText, container } = renderWithPageContext(<ResetPassword />); | ||
const submitButton = getByText('action.confirm'); | ||
const passwordInput = container.querySelector('input[name="new-password"]'); | ||
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]'); | ||
|
||
act(() => { | ||
if (passwordInput) { | ||
fireEvent.change(passwordInput, { target: { value: '123456' } }); | ||
} | ||
|
||
if (confirmPasswordInput) { | ||
fireEvent.change(confirmPasswordInput, { target: { value: '012345' } }); | ||
} | ||
|
||
fireEvent.click(submitButton); | ||
}); | ||
|
||
expect(queryByText('passwords_do_not_match')).not.toBeNull(); | ||
|
||
expect(resetPassword).not.toBeCalled(); | ||
|
||
act(() => { | ||
// Clear Error | ||
if (confirmPasswordInput) { | ||
fireEvent.change(confirmPasswordInput, { target: { value: '123456' } }); | ||
} | ||
}); | ||
|
||
expect(queryByText('passwords_do_not_match')).toBeNull(); | ||
}); | ||
|
||
test('should submit properly', async () => { | ||
const { queryByText, getByText, container } = renderWithPageContext(<ResetPassword />); | ||
const submitButton = getByText('action.confirm'); | ||
const passwordInput = container.querySelector('input[name="new-password"]'); | ||
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]'); | ||
|
||
act(() => { | ||
if (passwordInput) { | ||
fireEvent.change(passwordInput, { target: { value: '123456' } }); | ||
} | ||
|
||
if (confirmPasswordInput) { | ||
fireEvent.change(confirmPasswordInput, { target: { value: '123456' } }); | ||
} | ||
|
||
fireEvent.click(submitButton); | ||
}); | ||
|
||
expect(queryByText('passwords_do_not_match')).toBeNull(); | ||
|
||
await waitFor(() => { | ||
expect(resetPassword).toBeCalled(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import classNames from 'classnames'; | ||
import { useEffect, useCallback } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
|
||
import { resetPassword } from '@/apis/reset-password'; | ||
import Button from '@/components/Button'; | ||
import Input from '@/components/Input'; | ||
import useApi from '@/hooks/use-api'; | ||
import useForm from '@/hooks/use-form'; | ||
import { passwordValidation, confirmPasswordValidation } from '@/utils/field-validations'; | ||
|
||
import * as styles from './index.module.scss'; | ||
|
||
type Props = { | ||
className?: string; | ||
// eslint-disable-next-line react/boolean-prop-naming | ||
autoFocus?: boolean; | ||
}; | ||
|
||
type FieldState = { | ||
password: string; | ||
confirmPassword: string; | ||
}; | ||
|
||
const defaultState: FieldState = { | ||
password: '', | ||
confirmPassword: '', | ||
}; | ||
|
||
const ResetPassword = ({ className, autoFocus }: Props) => { | ||
const { t } = useTranslation(); | ||
|
||
const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState); | ||
|
||
const { result, run: asyncRegister } = useApi(resetPassword); | ||
|
||
const onSubmitHandler = useCallback( | ||
async (event?: React.FormEvent<HTMLFormElement>) => { | ||
event?.preventDefault(); | ||
|
||
if (!validateForm()) { | ||
return; | ||
} | ||
|
||
void asyncRegister(fieldValue.password); | ||
}, | ||
[validateForm, asyncRegister, fieldValue] | ||
); | ||
|
||
useEffect(() => { | ||
if (result?.redirectTo) { | ||
window.location.replace(result.redirectTo); | ||
} | ||
}, [result]); | ||
|
||
return ( | ||
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}> | ||
<Input | ||
className={styles.inputField} | ||
name="new-password" | ||
type="password" | ||
autoComplete="new-password" | ||
placeholder={t('input.password')} | ||
autoFocus={autoFocus} | ||
{...register('password', passwordValidation)} | ||
onClear={() => { | ||
setFieldValue((state) => ({ ...state, password: '' })); | ||
}} | ||
/> | ||
<Input | ||
className={styles.inputField} | ||
name="confirm-new-password" | ||
type="password" | ||
placeholder={t('input.confirm_password')} | ||
{...register('confirmPassword', (confirmPassword) => | ||
confirmPasswordValidation(fieldValue.password, confirmPassword) | ||
)} | ||
errorStyling={false} | ||
onClear={() => { | ||
setFieldValue((state) => ({ ...state, confirmPassword: '' })); | ||
}} | ||
/> | ||
|
||
<Button onClick={async () => onSubmitHandler()}>{t('action.confirm')}</Button> | ||
|
||
<input hidden type="submit" /> | ||
</form> | ||
); | ||
}; | ||
|
||
export default ResetPassword; |
Oops, something went wrong.