From e644249193fa1c0c25637e10d8cac68ed536628a Mon Sep 17 00:00:00 2001 From: Andrei Dekterev Date: Wed, 2 Feb 2022 18:51:39 +0700 Subject: [PATCH] feat(#2199): add recovery pass popup and change password page --- .../AuthServiceApp/components/App/index.tsx | 5 + .../components/ChangePassword/hooks.tsx | 87 +++++++++++ .../components/ChangePassword/index.tsx | 83 ++++++++++ .../AuthServiceApp/components/Login/hooks.tsx | 9 ++ .../AuthServiceApp/components/Login/index.tsx | 18 ++- .../components/RecoveryPopup/hooks.tsx | 62 ++++++++ .../components/RecoveryPopup/index.tsx | 87 +++++++++++ .../components/RecoveryPopup/styled.tsx | 147 ++++++++++++++++++ src/features/AuthServiceApp/config/lexics.tsx | 12 ++ src/features/AuthServiceApp/config/pages.tsx | 1 + .../AuthServiceApp/hooks/useAuthFields.tsx | 1 + .../requests/changePassword.tsx | 38 +++++ .../AuthServiceApp/requests/loginCheck.tsx | 35 +++++ src/features/AuthServiceApp/styled.tsx | 30 +++- 14 files changed, 609 insertions(+), 6 deletions(-) create mode 100644 src/features/AuthServiceApp/components/ChangePassword/hooks.tsx create mode 100644 src/features/AuthServiceApp/components/ChangePassword/index.tsx create mode 100644 src/features/AuthServiceApp/components/RecoveryPopup/hooks.tsx create mode 100644 src/features/AuthServiceApp/components/RecoveryPopup/index.tsx create mode 100644 src/features/AuthServiceApp/components/RecoveryPopup/styled.tsx create mode 100644 src/features/AuthServiceApp/requests/changePassword.tsx create mode 100644 src/features/AuthServiceApp/requests/loginCheck.tsx diff --git a/src/features/AuthServiceApp/components/App/index.tsx b/src/features/AuthServiceApp/components/App/index.tsx index dfb2dc8f..b56fb81b 100644 --- a/src/features/AuthServiceApp/components/App/index.tsx +++ b/src/features/AuthServiceApp/components/App/index.tsx @@ -17,6 +17,7 @@ import { lexics } from '../../config/lexics' const Login = lazy(() => import('../Login')) const Registration = lazy(() => import('../Registration')) +const ChangePassword = lazy(() => import('../ChangePassword')) const Main = styled.main` width: 100%; @@ -44,6 +45,10 @@ export const App = () => { + + + + diff --git a/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx b/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx new file mode 100644 index 00000000..abcd86c9 --- /dev/null +++ b/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx @@ -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) => { + if (!checkPassword(value)) { + setError('check_password') + } else { + setError('') + } + setPassword(value) + } + + const onConfirmPasswordChange = ({ + target: { value }, + }: ChangeEvent) => { + 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, + } +} diff --git a/src/features/AuthServiceApp/components/ChangePassword/index.tsx b/src/features/AuthServiceApp/components/ChangePassword/index.tsx new file mode 100644 index 00000000..3cda92c5 --- /dev/null +++ b/src/features/AuthServiceApp/components/ChangePassword/index.tsx @@ -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 ( + + +
+ + + + + + + + + + + + + + + { + isFetching + ? + : + } + + + +
+ ) +} + +export default ChangePassword diff --git a/src/features/AuthServiceApp/components/Login/hooks.tsx b/src/features/AuthServiceApp/components/Login/hooks.tsx index 8d5b9102..d624dec1 100644 --- a/src/features/AuthServiceApp/components/Login/hooks.tsx +++ b/src/features/AuthServiceApp/components/Login/hooks.tsx @@ -19,6 +19,8 @@ export const useLoginForm = () => { const { lang } = useLexicsStore() const [authError, setAuthError] = useState('') const [isFetching, setIsFetching] = useState(false) + const [isModalOpen, setIsModalOpen] = useState(false) + const formRef = useRef(null) const { email, @@ -38,6 +40,10 @@ export const useLoginForm = () => { || isFetching ) + const handleModalOpen = () => { + setIsModalOpen(true) + } + const submitForm = () => formRef.current?.submit() const handleError = (error: string) => { @@ -62,14 +68,17 @@ export const useLoginForm = () => { email, formError, formRef, + handleModalOpen, handleSubmit, isFetching, + isModalOpen, isSubmitDisabled, onEmailBlur, onEmailChange, onPasswordBlur, onPasswordChange, password, + setIsModalOpen, url: addLanguageUrlParam(lang, url), } } diff --git a/src/features/AuthServiceApp/components/Login/index.tsx b/src/features/AuthServiceApp/components/Login/index.tsx index 47a598de..5990918a 100644 --- a/src/features/AuthServiceApp/components/Login/index.tsx +++ b/src/features/AuthServiceApp/components/Login/index.tsx @@ -1,5 +1,6 @@ import { T9n } from 'features/T9n' import { ArrowLoader } from 'features/ArrowLoader' +import { RecoveryPopup } from 'features/AuthServiceApp/components/RecoveryPopup' import { PAGES } from '../../config/pages' import { LanguageSelect } from '../LanguageSelect' @@ -11,7 +12,9 @@ import { InputGroup, BlockTitle, CenterBlock, + Container, ButtonsBlock, + ForgotPass, Form, ButtonSolid, Error, @@ -25,14 +28,17 @@ const Login = () => { email, formError, formRef, + handleModalOpen, handleSubmit, isFetching, + isModalOpen, isSubmitDisabled, onEmailBlur, onEmailChange, onPasswordBlur, onPasswordChange, password, + setIsModalOpen, url, } = useLoginForm() @@ -85,10 +91,16 @@ const Login = () => { - - - + + + + + + + + + ) } diff --git a/src/features/AuthServiceApp/components/RecoveryPopup/hooks.tsx b/src/features/AuthServiceApp/components/RecoveryPopup/hooks.tsx new file mode 100644 index 00000000..3f444e78 --- /dev/null +++ b/src/features/AuthServiceApp/components/RecoveryPopup/hooks.tsx @@ -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) => { + 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, + } +} diff --git a/src/features/AuthServiceApp/components/RecoveryPopup/index.tsx b/src/features/AuthServiceApp/components/RecoveryPopup/index.tsx new file mode 100644 index 00000000..d4e57150 --- /dev/null +++ b/src/features/AuthServiceApp/components/RecoveryPopup/index.tsx @@ -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 ( + + +
+ + + +
+ + {isSendMessage ? ( + + + + ) : ( + + + + + + + + + )} +
+ + {(isFetching && ) + || (isSendMessage ? 'Ok' : )} + +
+ +
+
+ ) +} diff --git a/src/features/AuthServiceApp/components/RecoveryPopup/styled.tsx b/src/features/AuthServiceApp/components/RecoveryPopup/styled.tsx new file mode 100644 index 00000000..0a1354c0 --- /dev/null +++ b/src/features/AuthServiceApp/components/RecoveryPopup/styled.tsx @@ -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` + ${({ 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; +` diff --git a/src/features/AuthServiceApp/config/lexics.tsx b/src/features/AuthServiceApp/config/lexics.tsx index b69cbffb..e29b9ba6 100644 --- a/src/features/AuthServiceApp/config/lexics.tsx +++ b/src/features/AuthServiceApp/config/lexics.tsx @@ -1,4 +1,5 @@ export const lexics = { + change_password: 13442, check_email: 15907, check_password: 15842, confirm_2_hours: 15906, @@ -11,19 +12,30 @@ export const lexics = { error_invalid_email_or_password: 15774, error_invalid_platform: 15925, error_missing_required_argument: 15921, + error_passwords_missmatch: 15841, error_simple_password: 12940, error_unsupported_response_type: 15922, error_user_already_created: 15926, error_user_not_found: 15956, + error_user_not_found_recovery: 1417, + forgot_password: 1561, form_email: 12912, form_password: 751, go_back: 1907, i_accept: 15737, login: 13404, + password_new: 15056, + password_repeat: 15057, + recovery_email: 16238, register: 13328, + registration_email: 16239, registration_successful: 12945, + send: 16166, send_confirm: 15905, send_new_email: 15830, + send_new_email_for_change_pass: 14702, + set_new_password: 16240, + set_new_password_for_your_account: 15840, sign_up: 1305, step_title_login: 13404, step_title_registration: 1306, diff --git a/src/features/AuthServiceApp/config/pages.tsx b/src/features/AuthServiceApp/config/pages.tsx index 54f56915..9639849c 100644 --- a/src/features/AuthServiceApp/config/pages.tsx +++ b/src/features/AuthServiceApp/config/pages.tsx @@ -1,4 +1,5 @@ export const PAGES = { + change_password: '/change_password', login: '/authorize', registration: '/registration', } diff --git a/src/features/AuthServiceApp/hooks/useAuthFields.tsx b/src/features/AuthServiceApp/hooks/useAuthFields.tsx index 89831b9a..84165f4c 100644 --- a/src/features/AuthServiceApp/hooks/useAuthFields.tsx +++ b/src/features/AuthServiceApp/hooks/useAuthFields.tsx @@ -48,6 +48,7 @@ export const useAuthFields = (page: 'login'|'registration') => { } return { + checkPassword, email, error, onEmailBlur, diff --git a/src/features/AuthServiceApp/requests/changePassword.tsx b/src/features/AuthServiceApp/requests/changePassword.tsx new file mode 100644 index 00000000..e3aadf7d --- /dev/null +++ b/src/features/AuthServiceApp/requests/changePassword.tsx @@ -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]) +} diff --git a/src/features/AuthServiceApp/requests/loginCheck.tsx b/src/features/AuthServiceApp/requests/loginCheck.tsx new file mode 100644 index 00000000..1cdbd889 --- /dev/null +++ b/src/features/AuthServiceApp/requests/loginCheck.tsx @@ -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]) +} diff --git a/src/features/AuthServiceApp/styled.tsx b/src/features/AuthServiceApp/styled.tsx index cf5f4a6b..caa30398 100644 --- a/src/features/AuthServiceApp/styled.tsx +++ b/src/features/AuthServiceApp/styled.tsx @@ -45,7 +45,7 @@ export const Form = styled.form` ? css` width: 100%; margin-top: 78px; - @media screen and (orientation: landscape){ + @media screen and (orientation: landscape) { margin-bottom: 0; margin-top: 10px; padding: 0; @@ -61,7 +61,7 @@ export const BlockTitle = styled(T9n)` font-size: 24px; height: 24px; color: ${({ theme: { colors } }) => colors.text100}; - margin-bottom: 30px; + margin-bottom: 30px;, ${isMobileDevice ? css` font-size: 20px; @@ -120,6 +120,30 @@ export const Error = styled.span` ` export const LanguageSelectWrapper = styled.div` - margin-top: 15px; align-self: flex-end; ` + +export const ForgotPass = styled.div` + font-size: 16px; + color: #ffffff; + justify-content: flex-start; + width: 100%; + cursor: pointer; +` + +export const Container = styled.div` + min-width: 100%; + margin-top: 15px; + display: flex; + flex-direction: row; + align-items: center; +` +export const FormText = styled.span` + justify-content: flex-start; + text-align: center; + margin-bottom: 15px; + font-size: 16px; + color: #ffffff; + width: 100%; + cursor: pointer; +`