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 = { |
||||
change_password: '/change_password', |
||||
login: '/authorize', |
||||
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