From b1c71faf3ae12e1342281064bc80b01c87b4c215 Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Mon, 27 Jul 2020 18:17:48 +0700 Subject: [PATCH] attaching accounts and displaying errors --- .../profile/ProfileAccounts/index.tsx | 6 ++++ .../profile/ProfileAccounts/styles.scss | 7 +++- .../profile/ProfileAccountsError/index.tsx | 22 ++++++++++++ .../profile/ProfileAccountsError/styles.scss | 36 +++++++++++++++++++ src/constants/api.ts | 5 ++- src/constants/errors.ts | 3 ++ src/redux/auth/api.ts | 18 ++++++++-- src/redux/auth/sagas.ts | 36 +++++++++++++++++-- src/utils/api/index.ts | 25 ++++++------- 9 files changed, 140 insertions(+), 18 deletions(-) create mode 100644 src/components/profile/ProfileAccountsError/index.tsx create mode 100644 src/components/profile/ProfileAccountsError/styles.scss diff --git a/src/components/profile/ProfileAccounts/index.tsx b/src/components/profile/ProfileAccounts/index.tsx index fa239dbf..4c20543f 100644 --- a/src/components/profile/ProfileAccounts/index.tsx +++ b/src/components/profile/ProfileAccounts/index.tsx @@ -10,6 +10,7 @@ import { selectAuthProfile } from '~/redux/auth/selectors'; import { IState } from '~/redux/store'; import { connect } from 'react-redux'; import { API } from '~/constants/api'; +import { ProfileAccountsError } from '~/components/profile/ProfileAccountsError'; const mapStateToProps = (state: IState) => selectAuthProfile(state).socials; const mapDispatchToProps = { @@ -33,6 +34,7 @@ const ProfileAccountsUnconnected: FC = ({ authSetSocials, accounts, is_loading, + error, }) => { const onMessage = useCallback( (event: MessageEvent) => { @@ -57,6 +59,8 @@ const ProfileAccountsUnconnected: FC = ({ [] ); + const resetErrors = useCallback(() => authSetSocials({ error: '' }), [authSetSocials]); + useEffect(() => { authGetSocials(); }, [authGetSocials]); @@ -68,6 +72,8 @@ const ProfileAccountsUnconnected: FC = ({ return ( + {error && } +

Ты можешь входить в Убежище, используя аккаунты на других сайтах вместо ввода логина и diff --git a/src/components/profile/ProfileAccounts/styles.scss b/src/components/profile/ProfileAccounts/styles.scss index 8665f601..bdbb7351 100644 --- a/src/components/profile/ProfileAccounts/styles.scss +++ b/src/components/profile/ProfileAccounts/styles.scss @@ -3,7 +3,6 @@ } .list { - padding: $gap; box-shadow: inset transparentize(white, 0.9) 0 0 0 2px; border-radius: $radius; } @@ -38,6 +37,12 @@ grid-template-columns: 20px auto 20px; grid-column-gap: $gap * 1.5; align-items: center; + border-bottom: 1px solid transparentize(white, 0.9); + padding: $gap; + + &:last-child { + border-bottom: none; + } &__photo { width: 28px; diff --git a/src/components/profile/ProfileAccountsError/index.tsx b/src/components/profile/ProfileAccountsError/index.tsx new file mode 100644 index 00000000..706345fb --- /dev/null +++ b/src/components/profile/ProfileAccountsError/index.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; +import styles from './styles.scss'; +import { Group } from '~/components/containers/Group'; +import { ERROR_LITERAL } from '~/constants/errors'; +import { Button } from '~/components/input/Button'; + +interface IProps { + onClose: () => void; + error: string; +} + +const ProfileAccountsError: FC = ({ onClose, error }) => ( +

+ +
О НЕТ!
+
{ERROR_LITERAL[error] || error}
+ +
+
+); + +export { ProfileAccountsError }; diff --git a/src/components/profile/ProfileAccountsError/styles.scss b/src/components/profile/ProfileAccountsError/styles.scss new file mode 100644 index 00000000..b881f5fe --- /dev/null +++ b/src/components/profile/ProfileAccountsError/styles.scss @@ -0,0 +1,36 @@ +.wrap { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2; + background-color: transparentize(black, 0.5); + display: flex; + align-items: center; + justify-content: center; + + @include can_backdrop { + background-color: transparentize($content_bg, 0.5); + backdrop-filter: blur(10px); + } +} + +.title { + text-transform: capitalize; + font: $font_cell_title; +} + +.text { + padding: $gap 0 $gap * 3; +} + +.content { + max-width: 260px; + width: 100%; + color: white; + border-radius: $radius; + background-color: $content_bg; + padding: $gap * 2; + line-height: 1.2em; +} diff --git a/src/constants/api.ts b/src/constants/api.ts index 42d3f39d..47a8cf4d 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -7,7 +7,6 @@ export const API = { LOGIN: '/user/login', OAUTH_WINDOW: (provider: ISocialProvider) => `${process.env.API_HOST}oauth/${provider}/redirect`, - VKONTAKTE_LOGIN: `${process.env.API_HOST}/oauth/vkontakte/redirect/login`, ME: '/user/', PROFILE: (username: string) => `/user/user/${username}/profile`, MESSAGES: (username: string) => `/user/user/${username}/messages`, @@ -15,8 +14,12 @@ export const API = { GET_UPDATES: '/user/updates', REQUEST_CODE: (code?: string) => `/user/restore/${code || ''}`, UPLOAD: (target, type) => `/upload/${target}/${type}`, + GET_SOCIALS: '/oauth/', DROP_SOCIAL: (provider, id) => `/oauth/${provider}/${id}`, + ATTACH_SOCIAL: `/oauth/attach`, + // TODO: REMOVE + VKONTAKTE_LOGIN: `${process.env.API_HOST}/oauth/vkontakte/redirect/login`, }, NODE: { SAVE: '/node/', diff --git a/src/constants/errors.ts b/src/constants/errors.ts index 6d6d1f3d..0bf2ece7 100644 --- a/src/constants/errors.ts +++ b/src/constants/errors.ts @@ -20,6 +20,7 @@ export const ERRORS = { REQUIRED: 'Required', COMMENT_NOT_FOUND: 'Comment_Not_Found', FILE_IS_TOO_BIG: 'File_Is_Too_Big', + USER_EXIST_WITH_EMAIL: 'User_Exist_With_Email', }; export const ERROR_LITERAL = { @@ -44,4 +45,6 @@ export const ERROR_LITERAL = { [ERRORS.COMMENT_NOT_FOUND]: 'Комментарий не найден', [ERRORS.UNKNOWN_FILE_TYPE]: 'Запрещенный тип файла', [ERRORS.FILE_IS_TOO_BIG]: 'Файл слишком большой', + [ERRORS.USER_EXIST_WITH_EMAIL]: + 'Мы не можем продолжить, потому что у другого пользователя есть этот email', }; diff --git a/src/redux/auth/api.ts b/src/redux/auth/api.ts index 434f1245..1e63b5da 100644 --- a/src/redux/auth/api.ts +++ b/src/redux/auth/api.ts @@ -1,6 +1,6 @@ -import { api, errorMiddleware, resultMiddleware, configWithToken } from '~/utils/api'; +import { api, configWithToken, errorMiddleware, resultMiddleware } from '~/utils/api'; import { API } from '~/constants/api'; -import { IResultWithStatus, IMessage, INotification } from '~/redux/types'; +import { IMessage, INotification, IResultWithStatus } from '~/redux/types'; import { userLoginTransform } from '~/redux/auth/transforms'; import { ISocialAccount, IUser } from './types'; @@ -115,3 +115,17 @@ export const apiDropSocial = ({ .delete(API.USER.DROP_SOCIAL(provider, id), configWithToken(access)) .then(resultMiddleware) .catch(errorMiddleware); + +export const apiAttachSocial = ({ + access, + token, +}: { + access: string; + token: string; +}): Promise> => + api + .post(API.USER.ATTACH_SOCIAL, { token }, configWithToken(access)) + .then(resultMiddleware) + .catch(errorMiddleware); diff --git a/src/redux/auth/sagas.ts b/src/redux/auth/sagas.ts index aab1395c..bc987464 100644 --- a/src/redux/auth/sagas.ts +++ b/src/redux/auth/sagas.ts @@ -1,9 +1,9 @@ import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects'; import { AUTH_USER_ACTIONS, EMPTY_USER, USER_ERRORS, USER_ROLES } from '~/redux/auth/constants'; import { + authAttachSocial, authDropSocial, authGetMessages, - authGetSocials, authLoadProfile, authLoggedIn, authOpenProfile, @@ -24,6 +24,7 @@ import { userSetLoginError, } from '~/redux/auth/actions'; import { + apiAttachSocial, apiAuthGetUpdates, apiAuthGetUser, apiAuthGetUserMessages, @@ -412,7 +413,37 @@ function* dropSocial({ provider, id }: ReturnType) { yield call(getSocials); } catch (e) { - yield put(authSetSocials({ error: e.toString() })); + yield put(authSetSocials({ error: e.message })); + } +} + +function* attachSocial({ token }: ReturnType) { + if (!token) return; + + try { + yield put(authSetSocials({ error: '', is_loading: true })); + + const { data, error }: Unwrap> = yield call( + reqWrapper, + apiAttachSocial, + { token } + ); + + if (error) { + throw new Error(error); + } + + const { + socials: { accounts }, + }: ReturnType = yield select(selectAuthProfile); + + if (accounts.some(it => it.id === data.account.id && it.provider === data.account.provider)) { + yield put(authSetSocials({ is_loading: false })); + } else { + yield put(authSetSocials({ is_loading: false, accounts: [...accounts, data.account] })); + } + } catch (e) { + yield put(authSetSocials({ is_loading: false, error: e.message })); } } @@ -434,6 +465,7 @@ function* authSaga() { yield takeLatest(AUTH_USER_ACTIONS.RESTORE_PASSWORD, restorePassword); yield takeLatest(AUTH_USER_ACTIONS.GET_SOCIALS, getSocials); yield takeLatest(AUTH_USER_ACTIONS.DROP_SOCIAL, dropSocial); + yield takeLatest(AUTH_USER_ACTIONS.ATTACH_SOCIAL, attachSocial); } export default authSaga; diff --git a/src/utils/api/index.ts b/src/utils/api/index.ts index 55969f7f..e3a914f8 100644 --- a/src/utils/api/index.ts +++ b/src/utils/api/index.ts @@ -4,7 +4,7 @@ import { API } from '~/constants/api'; import { store } from '~/redux/store'; import { IResultWithStatus } from '~/redux/types'; -export const authMiddleware = (r) => { +export const authMiddleware = r => { store.dispatch(push('/login')); return r; }; @@ -23,26 +23,27 @@ export const HTTP_RESPONSES = { TOO_MANY_REQUESTS: 429, }; -export const resultMiddleware = (({ +export const resultMiddleware = ({ status, data, }: { status: number; data: T; -}): { status: number; data: T } => ({ status, data })); +}): { status: number; data: T } => ({ status, data }); -export const errorMiddleware = (debug): IResultWithStatus => (debug && debug.response - ? debug.response - : { - status: HTTP_RESPONSES.CONNECTION_REFUSED, - data: {}, - debug, - error: 'Ошибка сети', - }); +export const errorMiddleware = (debug): IResultWithStatus => + debug && debug.response + ? debug.response.data || debug.response + : { + status: HTTP_RESPONSES.CONNECTION_REFUSED, + data: {}, + debug, + error: 'Ошибка сети', + }; export const configWithToken = ( access: string, - config: AxiosRequestConfig = {}, + config: AxiosRequestConfig = {} ): AxiosRequestConfig => ({ ...config, headers: { ...(config.headers || {}), Authorization: `Bearer ${access}` },