From 078c531e9367e860e44185b65d6579ba29b7543b Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Mon, 25 Nov 2019 16:52:01 +0700 Subject: [PATCH] password restore dialog --- src/constants/dialogs.ts | 2 + src/constants/errors.ts | 2 + src/containers/dialogs/LoginDialog/index.tsx | 2 - .../dialogs/RestorePasswordDialog/index.tsx | 160 ++++++++++++++++++ .../dialogs/RestorePasswordDialog/styles.scss | 59 +++++++ .../dialogs/RestoreRequestDialog/index.tsx | 6 +- src/index.tsx | 21 ++- src/redux/auth/actions.ts | 4 +- src/redux/auth/api.ts | 6 + src/redux/auth/constants.ts | 2 +- src/redux/auth/sagas.ts | 20 ++- src/redux/modal/constants.ts | 11 +- src/redux/modal/reducer.ts | 4 +- src/redux/modal/sagas.ts | 4 +- 14 files changed, 270 insertions(+), 33 deletions(-) create mode 100644 src/containers/dialogs/RestorePasswordDialog/index.tsx create mode 100644 src/containers/dialogs/RestorePasswordDialog/styles.scss diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index d0c6b5b7..0fd36b11 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -8,6 +8,7 @@ import { LoadingDialog } from '~/containers/dialogs/LoadingDialog'; import { TestDialog } from '~/containers/dialogs/TestDialog'; import { ProfileDialog } from '~/containers/dialogs/ProfileDialog'; import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog'; +import { RestorePasswordDialog } from '~/containers/dialogs/RestorePasswordDialog'; import { DIALOGS } from '~/redux/modal/constants'; export const DIALOG_CONTENT = { @@ -20,6 +21,7 @@ export const DIALOG_CONTENT = { [DIALOGS.TEST]: TestDialog, [DIALOGS.PROFILE]: ProfileDialog, [DIALOGS.RESTORE_REQUEST]: RestoreRequestDialog, + [DIALOGS.RESTORE_PASSWORD]: RestorePasswordDialog, }; export const NODE_EDITOR_DIALOGS = { diff --git a/src/constants/errors.ts b/src/constants/errors.ts index 4efef113..fbeddf34 100644 --- a/src/constants/errors.ts +++ b/src/constants/errors.ts @@ -15,6 +15,7 @@ export const ERRORS = { USER_EXIST: 'User_Exist', INCORRECT_PASSWORD: 'Incorrect_Password', CODE_IS_INVALID: 'Code_Is_Invalid', + DOESNT_MATCH: 'Doesnt_Match', }; export const ERROR_LITERAL = { @@ -34,4 +35,5 @@ export const ERROR_LITERAL = { [ERRORS.USER_EXIST]: 'Такой пользователь уже существует', [ERRORS.INCORRECT_PASSWORD]: 'Неправильный пароль', [ERRORS.CODE_IS_INVALID]: 'Код не существует или устарел', + [ERRORS.DOESNT_MATCH]: 'Пароли не совпадают', }; diff --git a/src/containers/dialogs/LoginDialog/index.tsx b/src/containers/dialogs/LoginDialog/index.tsx index fbb8726a..962c9803 100644 --- a/src/containers/dialogs/LoginDialog/index.tsx +++ b/src/containers/dialogs/LoginDialog/index.tsx @@ -25,8 +25,6 @@ const mapDispatchToProps = { type IProps = ReturnType & typeof mapDispatchToProps & IDialogProps & {}; -console.log('initial', MODAL_ACTIONS); - const LoginDialogUnconnected: FC = ({ onRequestClose, error, diff --git a/src/containers/dialogs/RestorePasswordDialog/index.tsx b/src/containers/dialogs/RestorePasswordDialog/index.tsx new file mode 100644 index 00000000..b010cc84 --- /dev/null +++ b/src/containers/dialogs/RestorePasswordDialog/index.tsx @@ -0,0 +1,160 @@ +import React, { FC, useState, useMemo, useCallback, useEffect } from 'react'; +import { IDialogProps } from '~/redux/types'; +import { connect } from 'react-redux'; +import { BetterScrollDialog } from '../BetterScrollDialog'; +import { Group } from '~/components/containers/Group'; +import { InputText } from '~/components/input/InputText'; +import { Button } from '~/components/input/Button'; +import styles from './styles.scss'; + +import * as AUTH_ACTIONS from '~/redux/auth/actions'; +import pick from 'ramda/es/pick'; +import { selectAuthRestore } from '~/redux/auth/selectors'; +import { ERROR_LITERAL, ERRORS } from '~/constants/errors'; +import { Icon } from '~/components/input/Icon'; +import { useCloseOnEscape } from '~/utils/hooks'; + +const mapStateToProps = state => ({ + restore: selectAuthRestore(state), +}); + +const mapDispatchToProps = pick(['authRequestRestoreCode', 'authSetRestore'], AUTH_ACTIONS); + +type IProps = IDialogProps & ReturnType & typeof mapDispatchToProps & {}; + +const RestorePasswordDialogUnconnected: FC = ({ + restore: { error, is_loading, is_succesfull, user }, + authSetRestore, + onRequestClose, + authRequestRestoreCode, +}) => { + const [password, setPassword] = useState(''); + const [password_again, setPasswordAgain] = useState(''); + + const doesnt_match = useMemo( + () => !password || !password_again || password.trim() !== password_again.trim(), + [password_again, password] + ); + + const onSubmit = useCallback( + event => { + event.preventDefault(); + + if (doesnt_match) return; + // if (!field) return; + // + // authRequestRestoreCode(field); + }, + [doesnt_match] + ); + + useEffect(() => { + if (error || is_succesfull) { + authSetRestore({ error: null, is_succesfull: false }); + } + }, [password, password_again]); + + const buttons = useMemo( + () => ( + + + + ), + [doesnt_match] + ); + + const overlay = useMemo( + () => + is_succesfull ? ( + + + +
Отлично, добро пожаловать домой!
+ +
+ + + + ) : null, + [is_succesfull] + ); + + const not_ready = useMemo(() => (is_loading && !user ?
: null), [ + is_loading, + user, + ]); + + const invalid_code = useMemo( + () => + !is_loading && !user ? ( + + + +
{ERROR_LITERAL[error || ERRORS.CODE_IS_INVALID]}
+ +
+ + + + ) : null, + [is_loading, user, error] + ); + + useCloseOnEscape(onRequestClose); + + return ( +
+ +
+ +
+ Пришло время сменить пароль{user && user.username && `, ~${user.username}`} +
+ + + + + + +

Новый пароль должен быть не короче 6 символов.

+

+ Вряд ли кто-нибудь будет пытаться нас взломать, но сложный пароль всегда лучше + простого. +

+
+
+
+
+
+ ); +}; + +const RestorePasswordDialog = connect( + mapStateToProps, + mapDispatchToProps +)(RestorePasswordDialogUnconnected); + +export { RestorePasswordDialog }; diff --git a/src/containers/dialogs/RestorePasswordDialog/styles.scss b/src/containers/dialogs/RestorePasswordDialog/styles.scss new file mode 100644 index 00000000..9c952724 --- /dev/null +++ b/src/containers/dialogs/RestorePasswordDialog/styles.scss @@ -0,0 +1,59 @@ +.wrap { + padding: $gap $gap $gap * 4; +} + +.buttons { + padding: $gap; +} + +.text { + font: $font_14_regular; + padding: $gap; + color: darken(white, 50%); +} + +.shade, +.error_shade { + @include outer_shadow(); + + background: $content_bg; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: $radius; + padding: $gap * 2; + box-sizing: border-box; + text-transform: uppercase; + font: $font_18_semibold; + text-align: center; + color: $wisegreen; + + svg { + fill: $wisegreen; + } +} + +.error_shade { + color: $red; + + svg { + fill: $red; + } +} + +.header { + font: $font_18_semibold; + text-transform: uppercase; + padding: $gap; + padding-bottom: $gap * 2; +} + +.spacer { + height: $gap * 4; +} diff --git a/src/containers/dialogs/RestoreRequestDialog/index.tsx b/src/containers/dialogs/RestoreRequestDialog/index.tsx index 139decdd..d066d89b 100644 --- a/src/containers/dialogs/RestoreRequestDialog/index.tsx +++ b/src/containers/dialogs/RestoreRequestDialog/index.tsx @@ -10,9 +10,9 @@ import styles from './styles.scss'; import * as AUTH_ACTIONS from '~/redux/auth/actions'; import pick from 'ramda/es/pick'; import { selectAuthRestore } from '~/redux/auth/selectors'; -import { LoaderCircle } from '~/components/input/LoaderCircle'; import { ERROR_LITERAL } from '~/constants/errors'; import { Icon } from '~/components/input/Icon'; +import { useCloseOnEscape } from '~/utils/hooks'; const mapStateToProps = state => ({ restore: selectAuthRestore(state), @@ -28,7 +28,7 @@ const RestoreRequestDialogUnconnected: FC = ({ onRequestClose, authRequestRestoreCode, }) => { - const [field, setField] = useState(); + const [field, setField] = useState(''); const onSubmit = useCallback( event => { @@ -76,6 +76,8 @@ const RestoreRequestDialogUnconnected: FC = ({ [is_succesfull] ); + useCloseOnEscape(onRequestClose); + return (
) => ({ restore, }); -export const authRestorePassword = (code: string) => ({ - type: AUTH_USER_ACTIONS.RESTORE_PASSWORD, +export const authShowRestoreModal = (code: string) => ({ + type: AUTH_USER_ACTIONS.SHOW_RESTORE_MODAL, code, }); diff --git a/src/redux/auth/api.ts b/src/redux/auth/api.ts index 46d6643c..f94b05e4 100644 --- a/src/redux/auth/api.ts +++ b/src/redux/auth/api.ts @@ -72,3 +72,9 @@ export const apiRequestRestoreCode = ({ field }): Promise> .post(API.USER.REQUEST_CODE(), { field }) .then(resultMiddleware) .catch(errorMiddleware); + +export const apiCheckRestoreCode = ({ code }): Promise> => + api + .get(API.USER.REQUEST_CODE(code)) + .then(resultMiddleware) + .catch(errorMiddleware); diff --git a/src/redux/auth/constants.ts b/src/redux/auth/constants.ts index d1556146..8d821cfb 100644 --- a/src/redux/auth/constants.ts +++ b/src/redux/auth/constants.ts @@ -21,7 +21,7 @@ export const AUTH_USER_ACTIONS = { SET_RESTORE: 'SET_RESTORE', REQUEST_RESTORE_CODE: 'REQUEST_RESTORE_CODE', - RESTORE_PASSWORD: 'RESTORE_PASSWORD', + SHOW_RESTORE_MODAL: 'SHOW_RESTORE_MODAL', }; export const USER_ERRORS = { diff --git a/src/redux/auth/sagas.ts b/src/redux/auth/sagas.ts index 2ab18134..d5674a47 100644 --- a/src/redux/auth/sagas.ts +++ b/src/redux/auth/sagas.ts @@ -14,7 +14,7 @@ import { authLoggedIn, authSetLastSeenMessages, authPatchUser, - authRestorePassword, + authShowRestoreModal, authSetRestore, authRequestRestoreCode, } from '~/redux/auth/actions'; @@ -27,6 +27,7 @@ import { apiAuthGetUpdates, apiUpdateUser, apiRequestRestoreCode, + apiCheckRestoreCode, } from '~/redux/auth/api'; import { modalSetShown, modalShowDialog } from '~/redux/modal/actions'; import { selectToken, selectAuthProfile, selectAuthUser, selectAuthUpdates } from './selectors'; @@ -290,7 +291,7 @@ function* requestRestoreCode({ field }: ReturnType) { +function* showRestoreModal({ code }: ReturnType) { if (!code && !code.length) { return yield put( authSetRestore({ @@ -299,6 +300,19 @@ function* restorePassword({ code }: ReturnType) { }) ); } + + yield put(authSetRestore({ user: null, is_loading: true })); + + const { error, data } = yield call(apiCheckRestoreCode, { code }); + + if (data.error || error || !data.user) { + return yield put( + authSetRestore({ is_loading: false, error: data.error || error || ERRORS.CODE_IS_INVALID }) + ); + } + + yield put(modalShowDialog(DIALOGS.RESTORE_PASSWORD)); + yield put(authSetRestore({ user: data.user, is_loading: false })); } function* authSaga() { @@ -313,7 +327,7 @@ function* authSaga() { yield takeLatest(AUTH_USER_ACTIONS.SEND_MESSAGE, sendMessage); yield takeLatest(AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES, setLastSeenMessages); yield takeLatest(AUTH_USER_ACTIONS.PATCH_USER, patchUser); - yield takeLatest(AUTH_USER_ACTIONS.RESTORE_PASSWORD, restorePassword); + yield takeLatest(AUTH_USER_ACTIONS.SHOW_RESTORE_MODAL, showRestoreModal); yield takeLatest(AUTH_USER_ACTIONS.REQUEST_RESTORE_CODE, requestRestoreCode); } diff --git a/src/redux/modal/constants.ts b/src/redux/modal/constants.ts index 07f723ef..b1e8e4e6 100644 --- a/src/redux/modal/constants.ts +++ b/src/redux/modal/constants.ts @@ -1,14 +1,4 @@ import { ValueOf } from '~/redux/types'; -import { LoginDialog } from '~/containers/dialogs/LoginDialog'; -import { LoadingDialog } from '~/containers/dialogs/LoadingDialog'; -import { EditorDialogImage } from '~/containers/editors/EditorDialogImage'; -import { EditorDialogText } from '~/containers/editors/EditorDialogText'; -import { EditorDialogVideo } from '~/containers/editors/EditorDialogVideo'; -import { EditorDialogAudio } from '~/containers/editors/EditorDialogAudio'; -import { NODE_TYPES } from '../node/constants'; -import { TestDialog } from '~/containers/dialogs/TestDialog'; -import { ProfileDialog } from '~/containers/dialogs/ProfileDialog'; -import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog'; export const DIALOGS = { EDITOR_IMAGE: 'EDITOR_IMAGE', @@ -19,6 +9,7 @@ export const DIALOGS = { LOADING: 'LOADING', PROFILE: 'PROFILE', RESTORE_REQUEST: 'RESTORE_REQUEST', + RESTORE_PASSWORD: 'RESTORE_PASSWORD', TEST: 'TEST', }; diff --git a/src/redux/modal/reducer.ts b/src/redux/modal/reducer.ts index 38b439b3..be90b850 100644 --- a/src/redux/modal/reducer.ts +++ b/src/redux/modal/reducer.ts @@ -9,8 +9,8 @@ export interface IModalState { } const INITIAL_STATE: IModalState = { - is_shown: true, - dialog: DIALOGS.RESTORE_REQUEST, + is_shown: false, + dialog: null, }; export default createReducer(INITIAL_STATE, MODAL_HANDLERS); diff --git a/src/redux/modal/sagas.ts b/src/redux/modal/sagas.ts index e3b24d07..36fa076c 100644 --- a/src/redux/modal/sagas.ts +++ b/src/redux/modal/sagas.ts @@ -1,6 +1,6 @@ import { takeEvery, put } from 'redux-saga/effects'; import { LocationChangeAction, LOCATION_CHANGE } from 'connected-react-router'; -import { authOpenProfile, authRestorePassword } from '../auth/actions'; +import { authOpenProfile, authShowRestoreModal } from '../auth/actions'; function* onPathChange({ payload: { @@ -14,7 +14,7 @@ function* onPathChange({ if (pathname.match(/^\/restore\/([\w\-]+)/)) { const [, code] = pathname.match(/^\/restore\/([\w\-]+)/); - return yield put(authRestorePassword(code)); + return yield put(authShowRestoreModal(code)); } }