import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects'; import { AUTH_USER_ACTIONS, EMPTY_USER, USER_ROLES } from '~/redux/auth/constants'; import { authAttachSocial, authDropSocial, authGotOauthLoginEvent, authLoadProfile, authLoggedIn, authLoginWithSocial, authOpenProfile, authPatchUser, authRequestRestoreCode, authRestorePassword, authSendRegisterSocial, authSetLastSeenMessages, authSetProfile, authSetRegisterSocial, authSetRegisterSocialErrors, authSetRestore, authSetSocials, authSetToken, authSetUpdates, authSetUser, authShowRestoreModal, gotAuthPostMessage, userSendLoginRequest, userSetLoginError, } from '~/redux/auth/actions'; import { apiAttachSocial, apiAuthGetUpdates, apiAuthGetUser, apiAuthGetUserProfile, apiCheckRestoreCode, apiDropSocial, apiGetSocials, apiLoginWithSocial, apiRequestRestoreCode, apiRestoreCode, apiUpdateUser, apiUserLogin, } from '~/redux/auth/api'; import { selectAuth, selectAuthProfile, selectAuthRegisterSocial, selectAuthRestore, selectAuthUpdates, selectAuthUser, } from './selectors'; import { OAUTH_EVENT_TYPES, Unwrap } from '../types'; import { REHYDRATE, RehydrateAction } from 'redux-persist'; import { ERRORS } from '~/constants/errors'; import { messagesSet } from '~/redux/messages/actions'; import { SagaIterator } from 'redux-saga'; import { isEmpty } from 'ramda'; import { AxiosError } from 'axios'; import { labGetUpdates } from '~/redux/lab/actions'; import { getMOBXStore } from '~/store'; import { Dialog } from '~/constants/modal'; const modalStore = getMOBXStore().modal; function* setTokenSaga({ token }: ReturnType) { localStorage.setItem('token', token); } function* sendLoginRequestSaga({ username, password }: ReturnType) { if (!username || !password) return; try { const { token, user }: Unwrap = yield call(apiUserLogin, { username, password, }); yield put(authSetToken(token)); yield put(authSetUser({ ...user, is_user: true })); yield put(authLoggedIn()); modalStore.hide(); } catch (error) { yield put(userSetLoginError(error.message)); } } function* refreshUser() { const { token }: ReturnType = yield select(selectAuth); if (!token) return; try { const { user }: Unwrap = yield call(apiAuthGetUser); yield put(authSetUser({ ...user, is_user: true })); } catch (e) { yield put( authSetUser({ ...EMPTY_USER, is_user: false, }) ); } } function* checkUserSaga({ key }: RehydrateAction) { if (key !== 'auth') return; yield call(refreshUser); } function* gotPostMessageSaga({ token }: ReturnType) { yield put(authSetToken(token)); yield call(refreshUser); const { current } = modalStore; if (current === Dialog.Login) { modalStore.hide(); } } function* logoutSaga() { yield put(authSetToken('')); yield put(authSetUser({ ...EMPTY_USER })); yield put( authSetUpdates({ last: '', notifications: [], }) ); } function* loadProfile({ username }: ReturnType): SagaIterator { yield put(authSetProfile({ is_loading: true })); try { const { user }: Unwrap = yield call(apiAuthGetUserProfile, { username, }); yield put(authSetProfile({ is_loading: false, user })); yield put(messagesSet({ messages: [] })); return true; } catch (error) { return false; } } function* openProfile({ username, tab = 'profile' }: ReturnType) { modalStore.setCurrent(Dialog.Profile); yield put(authSetProfile({ tab })); const success: Unwrap = yield call(loadProfile, authLoadProfile(username)); if (!success) { modalStore.hide(); } } function* getUpdates() { try { const user: ReturnType = yield select(selectAuthUser); if (!user || !user.is_user || user.role === USER_ROLES.GUEST || !user.id) return; const { current } = modalStore; const profile: ReturnType = yield select(selectAuthProfile); const { last, boris_commented_at }: ReturnType = yield select( selectAuthUpdates ); const exclude_dialogs = current === Dialog.Profile && profile.user?.id ? profile.user.id : 0; const data: Unwrap = yield call(apiAuthGetUpdates, { exclude_dialogs, last: last || user.last_seen_messages, }); if (data.notifications && data.notifications.length) { yield put( authSetUpdates({ last: data.notifications[0].created_at, notifications: data.notifications, }) ); } if (data.boris && data.boris.commented_at && boris_commented_at !== data.boris.commented_at) { yield put( authSetUpdates({ boris_commented_at: data.boris.commented_at, }) ); } } catch (error) {} } function* startPollingSaga() { while (true) { yield call(getUpdates); yield put(labGetUpdates()); yield delay(60000); } } function* setLastSeenMessages({ last_seen_messages }: ReturnType) { if (!Date.parse(last_seen_messages)) return; yield call(apiUpdateUser, { user: { last_seen_messages } }); } function* patchUser(payload: ReturnType) { const me: ReturnType = yield select(selectAuthUser); try { const { user }: Unwrap = yield call(apiUpdateUser, { user: payload.user, }); yield put(authSetUser({ ...me, ...user })); yield put(authSetProfile({ user: { ...me, ...user }, tab: 'profile' })); } catch (error) { if (isEmpty(error.response.data.errors)) return; yield put(authSetProfile({ patch_errors: error.response.data.errors })); } } function* requestRestoreCode({ field }: ReturnType) { if (!field) return; try { yield put(authSetRestore({ error: '', is_loading: true })); yield call(apiRequestRestoreCode, { field, }); yield put(authSetRestore({ is_loading: false, is_succesfull: true })); } catch (error) { return yield put(authSetRestore({ is_loading: false, error: error.message })); } } function* showRestoreModal({ code }: ReturnType) { try { if (!code && !code.length) { return yield put(authSetRestore({ error: ERRORS.CODE_IS_INVALID, is_loading: false })); } yield put(authSetRestore({ user: undefined, is_loading: true })); const data: Unwrap = yield call(apiCheckRestoreCode, { code }); yield put(authSetRestore({ user: data.user, code, is_loading: false })); modalStore.setCurrent(Dialog.RestoreRequest); } catch (error) { yield put( authSetRestore({ is_loading: false, error: error.message || ERRORS.CODE_IS_INVALID }) ); modalStore.setCurrent(Dialog.RestoreRequest); } } function* restorePassword({ password }: ReturnType) { try { if (!password) return; yield put(authSetRestore({ is_loading: true })); const { code }: ReturnType = yield select(selectAuthRestore); if (!code) { return yield put(authSetRestore({ error: ERRORS.CODE_IS_INVALID, is_loading: false })); } const data: Unwrap = yield call(apiRestoreCode, { code, password }); yield put(authSetToken(data.token)); yield put(authSetUser(data.user)); yield put(authSetRestore({ is_loading: false, is_succesfull: true, error: '' })); yield call(refreshUser); } catch (error) { return yield put( authSetRestore({ is_loading: false, error: error.message || ERRORS.CODE_IS_INVALID }) ); } } function* getSocials() { try { yield put(authSetSocials({ is_loading: true, error: '' })); const data: Unwrap = yield call(apiGetSocials); yield put(authSetSocials({ accounts: data.accounts })); } catch (error) { yield put(authSetSocials({ error: error.message })); } finally { yield put(authSetSocials({ is_loading: false })); } } // TODO: start from here function* dropSocial({ provider, id }: ReturnType) { try { yield put(authSetSocials({ error: '' })); yield call(apiDropSocial, { id, provider, }); yield call(getSocials); } catch (error) { yield put(authSetSocials({ error: error.message })); } } function* attachSocial({ token }: ReturnType) { try { if (!token) return; yield put(authSetSocials({ error: '', is_loading: true })); const data: Unwrap = yield call(apiAttachSocial, { token, }); const { socials: { accounts }, }: ReturnType = yield select(selectAuthProfile); if (accounts.some(it => it.id === data.account.id && it.provider === data.account.provider)) { return; } yield put(authSetSocials({ accounts: [...accounts, data.account] })); } catch (e) { yield put(authSetSocials({ error: e.message })); } finally { yield put(authSetSocials({ is_loading: false })); } } function* loginWithSocial({ token }: ReturnType) { try { yield put(userSetLoginError('')); const data: Unwrap = yield call(apiLoginWithSocial, { token, }); if (data.token) { yield put(authSetToken(data.token)); yield call(refreshUser); modalStore.hide(); return; } } catch (error) { const { current } = modalStore; const data = (error as AxiosError<{ needs_register: boolean; errors: Record<'username' | 'password', string>; }>).response?.data; // Backend asks us for account registration if (current !== Dialog.LoginSocialRegister && data?.needs_register) { yield put(authSetRegisterSocial({ token })); modalStore.setCurrent(Dialog.LoginSocialRegister); return; } yield put(userSetLoginError(error.message)); } } function* gotOauthLoginEvent({ event }: ReturnType) { if (!event?.type) return; switch (event.type) { case OAUTH_EVENT_TYPES.OAUTH_PROCESSED: return yield put(authLoginWithSocial(event?.payload?.token)); case OAUTH_EVENT_TYPES.OAUTH_ERROR: return yield put(userSetLoginError(event?.payload?.error)); default: return; } } function* authRegisterSocial({ username, password }: ReturnType) { try { yield put(authSetRegisterSocial({ error: '' })); const { token }: ReturnType = yield select( selectAuthRegisterSocial ); const data: Unwrap = yield call(apiLoginWithSocial, { token, username, password, }); if (data.token) { yield put(authSetToken(data.token)); yield call(refreshUser); modalStore.hide(); return; } } catch (error) { const data = (error as AxiosError<{ needs_register: boolean; errors: Record<'username' | 'password', string>; }>).response?.data; if (data?.errors) { yield put(authSetRegisterSocialErrors(data.errors)); return; } yield put(authSetRegisterSocial({ error: error.message })); } } function* authSaga() { yield takeEvery(REHYDRATE, checkUserSaga); yield takeLatest([REHYDRATE, AUTH_USER_ACTIONS.LOGGED_IN], startPollingSaga); yield takeLatest(AUTH_USER_ACTIONS.LOGOUT, logoutSaga); yield takeLatest(AUTH_USER_ACTIONS.SET_TOKEN, setTokenSaga); yield takeLatest(AUTH_USER_ACTIONS.SEND_LOGIN_REQUEST, sendLoginRequestSaga); yield takeLatest(AUTH_USER_ACTIONS.GOT_AUTH_POST_MESSAGE, gotPostMessageSaga); yield takeLatest(AUTH_USER_ACTIONS.OPEN_PROFILE, openProfile); yield takeLatest(AUTH_USER_ACTIONS.LOAD_PROFILE, loadProfile); yield takeLatest(AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES, setLastSeenMessages); yield takeLatest(AUTH_USER_ACTIONS.PATCH_USER, patchUser); yield takeLatest(AUTH_USER_ACTIONS.REQUEST_RESTORE_CODE, requestRestoreCode); yield takeLatest(AUTH_USER_ACTIONS.SHOW_RESTORE_MODAL, showRestoreModal); 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); yield takeLatest(AUTH_USER_ACTIONS.LOGIN_WITH_SOCIAL, loginWithSocial); yield takeEvery(AUTH_USER_ACTIONS.GOT_OAUTH_LOGIN_EVENT, gotOauthLoginEvent); yield takeEvery(AUTH_USER_ACTIONS.SEND_REGISTER_SOCIAL, authRegisterSocial); } export default authSaga;