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 (
+
+
+
+
+ )
+}
+
+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 ? (
+
+
+
+ ) : (
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+ )
+}
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;
+`