feat(#2199): add recovery pass popup and change password page
parent
6db5a027c1
commit
e644249193
@ -0,0 +1,87 @@ |
|||||||
|
import { |
||||||
|
ChangeEvent, |
||||||
|
SyntheticEvent, |
||||||
|
useState, |
||||||
|
} from 'react' |
||||||
|
import { useLocation } from 'react-router-dom' |
||||||
|
|
||||||
|
import { useAuthFields } from 'features/AuthServiceApp/hooks/useAuthFields' |
||||||
|
import { changePassword } from 'features/AuthServiceApp/requests/changePassword' |
||||||
|
|
||||||
|
export const useChangePasswordForm = () => { |
||||||
|
const [error, setError] = useState('') |
||||||
|
const [isFetching, setIsFetching] = useState(false) |
||||||
|
const [password, setPassword] = useState('') |
||||||
|
const [confirmPassword, setConfirmPassword] = useState('') |
||||||
|
|
||||||
|
const { |
||||||
|
checkPassword, |
||||||
|
} = useAuthFields('login') |
||||||
|
|
||||||
|
const { search } = useLocation() |
||||||
|
|
||||||
|
const getUrlParam = (key: string) => { |
||||||
|
const token = new URLSearchParams(search).get(key) |
||||||
|
return token |
||||||
|
} |
||||||
|
|
||||||
|
const token = getUrlParam('token') |
||||||
|
|
||||||
|
const handleSubmit = async (event: SyntheticEvent) => { |
||||||
|
event?.preventDefault() |
||||||
|
setIsFetching(true) |
||||||
|
try { |
||||||
|
const redirectUrl = await changePassword(password, token || '') |
||||||
|
setError('') |
||||||
|
setIsFetching(false) |
||||||
|
if (redirectUrl) { |
||||||
|
await window.location.assign(redirectUrl) |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
setError(String(err)) |
||||||
|
setIsFetching(false) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const onPasswordChange = ({ |
||||||
|
target: { value }, |
||||||
|
}: ChangeEvent<HTMLInputElement>) => { |
||||||
|
if (!checkPassword(value)) { |
||||||
|
setError('check_password') |
||||||
|
} else { |
||||||
|
setError('') |
||||||
|
} |
||||||
|
setPassword(value) |
||||||
|
} |
||||||
|
|
||||||
|
const onConfirmPasswordChange = ({ |
||||||
|
target: { value }, |
||||||
|
}: ChangeEvent<HTMLInputElement>) => { |
||||||
|
setConfirmPassword(value) |
||||||
|
|
||||||
|
if (isMatchPasswords(password, value)) { |
||||||
|
setError('') |
||||||
|
} else { |
||||||
|
setError('error_passwords_missmatch') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const isMatchPasswords = (pass1: string, pass2: string) => pass1 === pass2 |
||||||
|
|
||||||
|
const isSubmitDisabled = !password |
||||||
|
|| !confirmPassword |
||||||
|
|| !isMatchPasswords(password, confirmPassword) |
||||||
|
|| Boolean(error) |
||||||
|
|| isFetching |
||||||
|
|
||||||
|
return { |
||||||
|
confirmPassword, |
||||||
|
error, |
||||||
|
handleSubmit, |
||||||
|
isFetching, |
||||||
|
isSubmitDisabled, |
||||||
|
onConfirmPasswordChange, |
||||||
|
onPasswordChange, |
||||||
|
password, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,83 @@ |
|||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { ArrowLoader } from 'features/ArrowLoader' |
||||||
|
|
||||||
|
import { PasswordInput } from '../PasswordInput' |
||||||
|
import { Logo } from '../Logo' |
||||||
|
import { useChangePasswordForm } from './hooks' |
||||||
|
import { |
||||||
|
BlockTitle, |
||||||
|
CenterBlock, |
||||||
|
FormText, |
||||||
|
InputGroup, |
||||||
|
ButtonsBlock, |
||||||
|
Form, |
||||||
|
ButtonSolid, |
||||||
|
Error, |
||||||
|
} from '../../styled' |
||||||
|
|
||||||
|
const ChangePassword = () => { |
||||||
|
const { |
||||||
|
confirmPassword, |
||||||
|
error, |
||||||
|
// formError,
|
||||||
|
handleSubmit, |
||||||
|
isFetching, |
||||||
|
isSubmitDisabled, |
||||||
|
// onPasswordBlur,
|
||||||
|
onConfirmPasswordChange, |
||||||
|
onPasswordChange, |
||||||
|
password, |
||||||
|
} = useChangePasswordForm() |
||||||
|
|
||||||
|
return ( |
||||||
|
<CenterBlock> |
||||||
|
<Logo /> |
||||||
|
<Form |
||||||
|
// method='PUT'
|
||||||
|
// // ref={formRef}
|
||||||
|
// // action={url}
|
||||||
|
onSubmit={handleSubmit} |
||||||
|
> |
||||||
|
<BlockTitle t='set_new_password' /> |
||||||
|
<FormText> |
||||||
|
<T9n t='set_new_password_for_your_account' /> |
||||||
|
</FormText> |
||||||
|
<InputGroup> |
||||||
|
<PasswordInput |
||||||
|
type='password' |
||||||
|
name='password' |
||||||
|
autoComplete='current-password' |
||||||
|
placeholderLexic='password_new' |
||||||
|
value={password} |
||||||
|
onChange={onPasswordChange} |
||||||
|
// onBlur={onPasswordBlur}
|
||||||
|
/> |
||||||
|
<PasswordInput |
||||||
|
type='password' |
||||||
|
name='confirm_password' |
||||||
|
autoComplete='current-password' |
||||||
|
placeholderLexic='password_repeat' |
||||||
|
value={confirmPassword} |
||||||
|
onChange={onConfirmPasswordChange} |
||||||
|
// onBlur={onPasswordBlur}
|
||||||
|
/> |
||||||
|
</InputGroup> |
||||||
|
<Error> |
||||||
|
<T9n t={error} /> |
||||||
|
</Error> |
||||||
|
|
||||||
|
<ButtonsBlock> |
||||||
|
<ButtonSolid disabled={isSubmitDisabled}> |
||||||
|
{ |
||||||
|
isFetching |
||||||
|
? <ArrowLoader /> |
||||||
|
: <T9n t='change_password' /> |
||||||
|
} |
||||||
|
</ButtonSolid> |
||||||
|
</ButtonsBlock> |
||||||
|
</Form> |
||||||
|
</CenterBlock> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default ChangePassword |
||||||
@ -0,0 +1,62 @@ |
|||||||
|
import { ChangeEvent, useState } from 'react' |
||||||
|
|
||||||
|
import { isValidEmail } from 'features/AuthServiceApp/helpers/isValidEmail' |
||||||
|
import { loginCheckChangePass } from 'features/AuthServiceApp/requests/loginCheck' |
||||||
|
|
||||||
|
export const useRecovery = (setIsModalOpen: (value: boolean) => void) => { |
||||||
|
const [isSendBtnDisabled, setIsSendBtnDisabled] = useState(true) |
||||||
|
const [error, setError] = useState('') |
||||||
|
const [email, setEmail] = useState('') |
||||||
|
const [isFetching, setIsFetching] = useState(false) |
||||||
|
const [isSendMessage, setIsSendMessage] = useState(false) |
||||||
|
|
||||||
|
const onEmailChange = ({ |
||||||
|
target: { value }, |
||||||
|
}: ChangeEvent<HTMLInputElement>) => { |
||||||
|
setError('') |
||||||
|
setEmail(value) |
||||||
|
if (isValidEmail(value)) { |
||||||
|
setIsSendBtnDisabled(false) |
||||||
|
} else { |
||||||
|
setIsSendBtnDisabled(true) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const closePopup = () => { |
||||||
|
setIsSendMessage(false) |
||||||
|
setIsFetching(false) |
||||||
|
setError('') |
||||||
|
setEmail('') |
||||||
|
setIsSendBtnDisabled(true) |
||||||
|
setIsModalOpen(false) |
||||||
|
} |
||||||
|
|
||||||
|
const handleError = (err: string) => { |
||||||
|
setError(err) |
||||||
|
setIsFetching(false) |
||||||
|
} |
||||||
|
|
||||||
|
const handleSubmit = () => { |
||||||
|
if (isSendMessage && !isFetching) { |
||||||
|
closePopup() |
||||||
|
} else { |
||||||
|
setIsFetching(true) |
||||||
|
loginCheckChangePass(email) |
||||||
|
.then(() => setIsSendMessage(true)) |
||||||
|
.then(() => setIsFetching(false)) |
||||||
|
.catch(handleError) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
closePopup, |
||||||
|
email, |
||||||
|
error, |
||||||
|
handleSubmit, |
||||||
|
isFetching, |
||||||
|
isSendBtnDisabled, |
||||||
|
isSendMessage, |
||||||
|
// onEmailBlur,
|
||||||
|
onEmailChange, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,87 @@ |
|||||||
|
import { Fragment } from 'react' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { ArrowLoader } from 'features/ArrowLoader' |
||||||
|
|
||||||
|
import { useRecovery } from './hooks' |
||||||
|
|
||||||
|
import { |
||||||
|
Modal, |
||||||
|
Wrapper, |
||||||
|
Header, |
||||||
|
HeaderTitle, |
||||||
|
Footer, |
||||||
|
ApplyButton, |
||||||
|
Text, |
||||||
|
Body, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
import { Input } from '../Input' |
||||||
|
// import { Error } from '../../styled'
|
||||||
|
import { InputGroup, Error } from '../../styled' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
isModalOpen: boolean, |
||||||
|
setIsModalOpen: (value: boolean) => void, |
||||||
|
} |
||||||
|
|
||||||
|
export const RecoveryPopup = (props: Props) => { |
||||||
|
const { isModalOpen, setIsModalOpen } = props |
||||||
|
|
||||||
|
const { |
||||||
|
closePopup, |
||||||
|
error, |
||||||
|
// onEmailBlur,
|
||||||
|
handleSubmit, |
||||||
|
isFetching, |
||||||
|
isSendBtnDisabled, |
||||||
|
isSendMessage, |
||||||
|
onEmailChange, |
||||||
|
} = useRecovery(setIsModalOpen) |
||||||
|
|
||||||
|
return ( |
||||||
|
<Modal |
||||||
|
isOpen={isModalOpen} |
||||||
|
withCloseButton |
||||||
|
close={closePopup} |
||||||
|
> |
||||||
|
<Wrapper> |
||||||
|
<Header> |
||||||
|
<HeaderTitle> |
||||||
|
<T9n t='recovery_email' /> |
||||||
|
</HeaderTitle> |
||||||
|
</Header> |
||||||
|
<Body> |
||||||
|
{isSendMessage ? ( |
||||||
|
<Text> |
||||||
|
<T9n t='send_new_email_for_change_pass' /> |
||||||
|
</Text> |
||||||
|
) : ( |
||||||
|
<Fragment> |
||||||
|
<InputGroup> |
||||||
|
<Input |
||||||
|
autoFocus |
||||||
|
type='email' |
||||||
|
name='email' |
||||||
|
autoComplete='email' |
||||||
|
placeholderLexic='registration_email' |
||||||
|
onChange={onEmailChange} |
||||||
|
// onBlur={onEmailBlur}
|
||||||
|
/> |
||||||
|
</InputGroup> |
||||||
|
<Error> |
||||||
|
<T9n t={error} /> |
||||||
|
</Error> |
||||||
|
</Fragment> |
||||||
|
)} |
||||||
|
<Footer> |
||||||
|
<ApplyButton onClick={handleSubmit} disabled={isSendBtnDisabled}> |
||||||
|
{(isFetching && <ArrowLoader />) |
||||||
|
|| (isSendMessage ? 'Ok' : <T9n t='send' />)} |
||||||
|
</ApplyButton> |
||||||
|
</Footer> |
||||||
|
</Body> |
||||||
|
</Wrapper> |
||||||
|
</Modal> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,147 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { isMobileDevice } from 'config/userAgent' |
||||||
|
import { devices } from 'config/devices' |
||||||
|
|
||||||
|
import { ModalWindow } from 'features/Modal/styled' |
||||||
|
import { Modal as BaseModal } from 'features/Modal' |
||||||
|
import { Header as BaseHeader } from 'features/PopupComponents' |
||||||
|
|
||||||
|
import { ButtonSolid } from 'features/Common' |
||||||
|
|
||||||
|
export const Modal = styled(BaseModal)` |
||||||
|
background-color: rgba(0, 0, 0, 0.7); |
||||||
|
padding: 0 60px; |
||||||
|
|
||||||
|
${ModalWindow} { |
||||||
|
width: 577px; |
||||||
|
max-width: 577px; |
||||||
|
max-height: 414px; |
||||||
|
padding-top: 60px; |
||||||
|
background-color: #333333; |
||||||
|
border-radius: 5px; |
||||||
|
|
||||||
|
@media (max-width: 1370px) { |
||||||
|
width: 70rem; |
||||||
|
height: auto; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
@media ${devices.mobile}{ |
||||||
|
height: auto; |
||||||
|
top: -7vh; |
||||||
|
} |
||||||
|
` |
||||||
|
: ''}; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
type WrapperProps = { |
||||||
|
isFetching?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const Wrapper = styled.div<WrapperProps>` |
||||||
|
${({ isFetching }) => ( |
||||||
|
isFetching |
||||||
|
? css`pointer-events: none;` |
||||||
|
: '' |
||||||
|
)} |
||||||
|
` |
||||||
|
|
||||||
|
export const Header = styled(BaseHeader)` |
||||||
|
height: auto; |
||||||
|
padding-top: 60; |
||||||
|
justify-content: center; |
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
@media ${devices.mobile}{ |
||||||
|
padding-top: 33px; |
||||||
|
} |
||||||
|
` |
||||||
|
: ''}; |
||||||
|
` |
||||||
|
|
||||||
|
export const HeaderTitle = styled.span` |
||||||
|
font-weight: 700; |
||||||
|
font-size: 24px; |
||||||
|
line-height: 24px; |
||||||
|
color: #FFFFFF; |
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
@media ${devices.mobile}{ |
||||||
|
font-size: 14px; |
||||||
|
line-height: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
@media (orientation: landscape) { |
||||||
|
font-size: 20px; |
||||||
|
} |
||||||
|
` |
||||||
|
: ''}; |
||||||
|
` |
||||||
|
|
||||||
|
export const Body = styled.div` |
||||||
|
padding: 25px 25px 0 25px; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
font-weight: normal; |
||||||
|
justify-content: center; |
||||||
|
font-size: 20px; |
||||||
|
line-height: 27px; |
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
@media ${devices.mobile}{ |
||||||
|
padding: 13px 25px 0; |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
@media (orientation: landscape){ |
||||||
|
padding: 22px 23px 0 29px; |
||||||
|
} |
||||||
|
` |
||||||
|
: ''}; |
||||||
|
` |
||||||
|
|
||||||
|
export const Footer = styled.div` |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
padding: 1.89rem; |
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
@media ${devices.mobile}{ |
||||||
|
flex-direction: column; |
||||||
|
padding: 20px 25px 20px; |
||||||
|
} |
||||||
|
` |
||||||
|
: ''}; |
||||||
|
` |
||||||
|
|
||||||
|
export const ApplyButton = styled(ButtonSolid)` |
||||||
|
max-width: 270px; |
||||||
|
border-radius: 5px; |
||||||
|
font-weight: 600; |
||||||
|
height: 50px; |
||||||
|
font-size: 20px; |
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
@media ${devices.mobile}{ |
||||||
|
width: 100%; |
||||||
|
min-height: 42px; |
||||||
|
margin-bottom: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
@media (orientation: landscape){ |
||||||
|
width: 290px; |
||||||
|
min-height: 42px; |
||||||
|
} |
||||||
|
` |
||||||
|
: ''}; |
||||||
|
` |
||||||
|
export const Text = styled.span` |
||||||
|
margin-bottom: 20px; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
` |
||||||
@ -1,4 +1,5 @@ |
|||||||
export const PAGES = { |
export const PAGES = { |
||||||
|
change_password: '/change_password', |
||||||
login: '/authorize', |
login: '/authorize', |
||||||
registration: '/registration', |
registration: '/registration', |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,38 @@ |
|||||||
|
import { getApiUrl } from '../config/routes' |
||||||
|
|
||||||
|
const errorLexics = { |
||||||
|
1: 'error_зassword mismatch', |
||||||
|
4: 'error_user_not_found', |
||||||
|
} |
||||||
|
|
||||||
|
type FailedResponse = { |
||||||
|
error: { |
||||||
|
code: keyof typeof errorLexics, |
||||||
|
message?: string, |
||||||
|
}, |
||||||
|
ok: false, |
||||||
|
} |
||||||
|
|
||||||
|
type SuccessResponse = { |
||||||
|
data: { |
||||||
|
url: string, |
||||||
|
}, |
||||||
|
ok: true, |
||||||
|
} |
||||||
|
|
||||||
|
export const changePassword = async (password: string, token: string) => { |
||||||
|
const url = getApiUrl(`/change_password?token=${token}`) |
||||||
|
const init: RequestInit = { |
||||||
|
body: new URLSearchParams({ |
||||||
|
password, |
||||||
|
}), |
||||||
|
method: 'PUT', |
||||||
|
} |
||||||
|
const response = await fetch(url, init) |
||||||
|
|
||||||
|
const body: SuccessResponse | FailedResponse = await response.json() |
||||||
|
|
||||||
|
if (body.ok) return body.data.url |
||||||
|
|
||||||
|
return Promise.reject(errorLexics[body.error.code]) |
||||||
|
} |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
import { getApiUrl } from '../config/routes' |
||||||
|
|
||||||
|
const errorLexics = { |
||||||
|
4: 'error_user_not_found_recovery', |
||||||
|
8: 'error_failed_to_send_email', |
||||||
|
} |
||||||
|
|
||||||
|
type FailedResponse = { |
||||||
|
error: { |
||||||
|
code: keyof typeof errorLexics, |
||||||
|
message?: string, |
||||||
|
}, |
||||||
|
ok: false, |
||||||
|
} |
||||||
|
|
||||||
|
type SuccessResponse = { |
||||||
|
ok: true, |
||||||
|
} |
||||||
|
|
||||||
|
export const loginCheckChangePass = async (email: string) => { |
||||||
|
const url = getApiUrl('/change_password') |
||||||
|
const init: RequestInit = { |
||||||
|
body: new URLSearchParams({ |
||||||
|
email, |
||||||
|
}), |
||||||
|
method: 'POST', |
||||||
|
} |
||||||
|
const response = await fetch(url, init) |
||||||
|
|
||||||
|
const body: SuccessResponse | FailedResponse = await response.json() |
||||||
|
|
||||||
|
if (body.ok) return Promise.resolve() |
||||||
|
|
||||||
|
return Promise.reject(errorLexics[body.error.code]) |
||||||
|
} |
||||||
Loading…
Reference in new issue