import { Observable } from 'rxjs' import { combineEpics, ActionsObservable } from 'redux-observable' import { LoginCreds, UpdateUserPropertiesOptions, ISubmitLoginSuccess } from './interfaces' import { Store } from 'redux' import * as Raven from 'raven-js' import '../../../img/bw_icon_32.png' import '../../../img/grey_icon_32.png' import * as api from './api' import * as actions from './actions' import * as selectors from './selectors' import * as SocketActions from '../socket/actions' import * as PlaybookActions from '../playbook/actions' import * as PopupActions from '../popup/actions' import { SocketCluster } from '../socket/constants' import { PatchOperation } from '../../interfaces/patch' import { version } from '../analytics/selectors' const login = (action$: ActionsObservable) => { return action$.ofType(actions.submitLogin.toString()) .pluck('payload') .mergeMap(({ username, password }: LoginCreds) => { Raven.setUserContext({ email: username }) return api .login(username, password) .map((value: string) => actions.submitLoginSuccess({ username, token: value })) .startWith(actions.submitLoginPending()) .catch(() => { return Observable.of(actions.submitLoginFailed()) }) }) } export const handleRavenUserContext = (action$: ActionsObservable) => { return action$.ofType(actions.submitLoginSuccess.toString()) .pluck('payload') .mergeMap(({ token }: ISubmitLoginSuccess) => { if (Raven.isSetup()) { Raven.setExtraContext({ token }) } return Observable.empty() }) } export const handleLoginSyncToken = (action$: ActionsObservable) => { return action$.ofType(actions.submitLoginSuccess.toString()) .pluck('payload') .mergeMap(({ token }: ISubmitLoginSuccess) => { chrome.storage.sync.set({ token }) chrome.browserAction.setBadgeText({ text: '' }) chrome.browserAction.setIcon({ path: 'bw_icon_32.png' }) return Observable.concat( Observable.of(PlaybookActions.searchPlaybooks()), Observable.of(PlaybookActions.fetchEnrolledSequenceCount()), ) }) } export const handleLogoutSyncToken = (action$: ActionsObservable) => { return action$.ofType(actions.logout.toString()) .mergeMap(() => { chrome.storage.sync.set({ token: null }) chrome.browserAction.setBadgeText({ text: '!' }) chrome.browserAction.setBadgeBackgroundColor({ color: '#394F5A' }) chrome.browserAction.setIcon({ path: 'grey_icon_32.png' }) return Observable.empty() }) } export const validateUserOnPopup = (action$: ActionsObservable, store: Store) => { return action$ .ofType(PopupActions.popupOpen.toString()) .map(() => selectors.accessToken(store.getState())) .filter(accessToken => !!accessToken) .mergeMap((accessToken) => { return api .fetchUser({ accessToken }) .catch((error) => { if (error.status === 403) { return Observable.of(actions.logout()) } else { return Observable.empty() } }) }) } export const handleLoginFetchUser = (action$: ActionsObservable) => { return action$.ofType(actions.submitLoginSuccess.toString()) .map(() => actions.fetchUser()) } export const handleLoginSocketAuthenticate = (action$: ActionsObservable) => { return action$.ofType(actions.fetchUserSuccess.toString()) .map(() => SocketActions.authenticate({ clusterName: SocketCluster.MESSAGES })) } export const handleLogoutSocketDisconnect = (action$: ActionsObservable) => { return action$.ofType(actions.logout.toString()) .map(() => SocketActions.disconnect()) } export const fetchUser = (action$: ActionsObservable, store: Store) => { return action$.ofType(actions.fetchUser.toString()) .map(() => ({ accessToken: selectors.accessToken(store.getState()) })) .mergeMap((options) => { return api.fetchUser(options) .map(response => actions.fetchUserSuccess(response)) .startWith(actions.fetchUserPending()) .catch(() => Observable.of(actions.fetchUserFailure())) }) } export const triggerUpdateExtensionInstalledEpic = (action$: ActionsObservable, store: Store) => { return action$.ofType(actions.fetchUserSuccess.toString()) .map(() => ({ operations: [ { property: 'extensionInstalled', operation: PatchOperation.Set, value: true }, { property: 'extensionLoginAt', operation: PatchOperation.Set, value: Date.now() }, { property: 'extensionVersion', operation: PatchOperation.Set, value: version(store.getState()) }, ] })) .map(options => actions.updateUserProperties(options)) } export const updateUserPropertiesEpic = (action$: ActionsObservable, store: Store) => { return action$.ofType(actions.updateUserProperties.toString()) .pluck('payload') .map((options: UpdateUserPropertiesOptions) => ({ accessToken: selectors.accessToken(store.getState()), userId: selectors.user(store.getState()).id, ...options, })) .mergeMap((options) => { return api.updateUserProperties(options) .map(response => actions.updateUserPropertiesSuccess(response)) .startWith(actions.updateUserPropertiesPending()) .catch(() => Observable.of(actions.updateUserPropertiesFailure())) }) } const fetchGoogleScopes = (action$: ActionsObservable, store: Store) => { return action$ .ofType(actions.fetchGoogleScopes.toString()) .switchMap(() => { return api .fetchGoogleScopes({ accessToken: selectors.accessToken(store.getState()) }) .map(response => actions.fetchGoogleScopesSuccess(response)) .startWith(actions.fetchGoogleScopesPending()) .catch(() => Observable.of(actions.fetchGoogleScopesFailed())) }) } export const epic = combineEpics( login, handleRavenUserContext, handleLoginSyncToken, handleLogoutSyncToken, handleLoginFetchUser, handleLoginSocketAuthenticate, handleLogoutSocketDisconnect, fetchUser, triggerUpdateExtensionInstalledEpic, updateUserPropertiesEpic, fetchGoogleScopes, validateUserOnPopup, )