Fragmented Thought

Redux sagas for Google YOLO

By

Published:

Lance Gliser

Heads up! This content is more than six months old. Take some time to verify everything still works as expected.

I've been working for a bit lately to get into React. Initially, I had built the stack: webpack, React, Redux, etc, all by hand. At some point I got over confident, and thought I was ready to go into the deep end. At that point, I swapped over into react boilerplate to 'really' develop something. Huge points won me over, hot reloading, 'some' guidance on the way it should be configured, and community to draw from.

More a sign of my over confidence, I decided I'd just start by doing an integration with Google's YOLO (You Only Login Once). The first major shock I received was the brand new concept of Redux Sagas. For those unfamiliar, it might be best summarized as an event driven system, a remix of RXJS and other subscription systems. It took quite a while to learn the basics, because they had examples that didn't match up to what felt like real world examples. To that end, I contribute my own little attempt. It's an alpha version, but it does show the details in a state you should be easily able to understand.

A major reason I never threw up my hands and walked away from this 'not tangled' (but totally complex snarl) is that I'm learning that everything really boils down to the state. The state is your source of truth in ways I was never used to in Angular. If it doesn't go through a reducer, it doesn't exist. That makes sagas very attractive. They can access the auth credentials, refresh them if required, set the update states, and all the rest without over involving the rendering of the component. The entire async / background / 'generator' function can be a bit of a rabbit hole all its own. For official documentation check out Mozilla's JavaScript generator functions. For now, please let your head work the general abstraction that if you see yield, take (any variety), race, or function*, background threads are issued and might return a value when it's damn well ready.

A fair warning, after the App component is mounted, things begin jumping file to file, fast. There are many paths that branch back onto each other. The best way to think of it conceptually is this:

Overview of purpose

  • saga.js - App business logic - Handles boot logic
  • saga.googleYolo.js - Google YOLO logic - Handles boot logic only if credentials are not found
  • saga.api.js - Api logic - No boot logic

Making an api call

// Inside your component's saga you can dispatch an api request action yield put(apiActions.apiUserDeleteRequestAction(user.username)); // You'll setup a race condition, waiting for success for failure // Note that the returned object deconstruction must match the object keys const { deleteAction, errorAction } = yield race({ deleteAction: take(API_USER_DELETE_REQUEST_SUCCESS), errorAction: take(API_USER_DELETE_REQUEST_FAILURE), }); // Alert your component's _state_ to the side effects. Your reducer is listening for this if (errorAction) { yield put(actions.userDeleteFailureAction(errorAction.error)); } if (deleteAction) { yield put(actions.userDeleteSuccessAction()); // This might well result in a redirect using something like: yield put(push('/auth')); }

Walkthrough of a single path

All this wouldn't be much use if I didn't provide a solid breakdown of how the thing functions. While there are multiple happy paths, really it's kind of all happy paths due to the handle of failures race() conditions encourage.

Let's outline this in English, and I'll highlight portions of the code after:

  • App component: constructor() -- Set the window.onload required by Google YOLO to fire an api loaded action
  • App - did mount -- Dispatch the app boot action

(At any point)

  • Google YOLO saga receives the api loaded notification, and sets state.googleYolo.api

(Following app boot)

  • App saga receives the app boot action
  • Redirect to home
  • Fire an independent process to read auth

(At any point)

  • App saga receives receives boot auth read
  • Checks for cookies
  • Fires app boot credentials unavailable action

(At the point both app boot credentials unavailable action AND Google YOLO api available have happened)

  • Google YOLO saga fires the googleYolo.retrieve() api

  • Google YOLO saga fires the api action authenticate with google account credentials

  • Google YOLO saga fires the app user identity request action

  • Api saga fires the app user identity success action

  • Google YOLO saga fires the user login success action

  • App saga redirects to the /profile page

  • phew - And that's the simple happy path

And again... in code

app/index.js - importing and composing the app to handle our sagas

import { withRouter } from "react-router-dom"; // ... import appSaga from "./saga"; import appReducer from "./reducer"; import googleYoloSaga from "./saga.googleYolo"; import googleYoloReducer from "./reducer.googleYolo"; import apiSaga from "./saga.api"; import apiReducer from "./reducer.api"; // ... export class App extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function // ... render() { // ... <Switch> {" "} // This will be activated by the withRouter part of our composition to allow saga yield push(put('/path')); <Route exact path="/" component={HomePage} /> <Route exact path="/auth" component={AuthPage} /> <Route exact path="/profile" component={ProfilePage} /> <Route component={NotFoundPage} /> </Switch>; // ... } } const withConnect = connect(mapStateToProps, mapDispatchToProps); const withAppReducer = injectReducer({ key: "app", reducer: appReducer }); const withAppSaga = injectSaga({ key: "app", saga: appSaga }); const withGoogleYoloReducer = injectReducer({ key: "googleYolo", reducer: googleYoloReducer, }); const withGoogleYoloSaga = injectSaga({ key: "googleYolo", saga: googleYoloSaga, }); const withApiReducer = injectReducer({ key: "api", reducer: apiReducer }); const withApiSaga = injectSaga({ key: "api", saga: apiSaga }); export default compose( withAppReducer, withAppSaga, withGoogleYoloReducer, withGoogleYoloSaga, withApiReducer, withApiSaga, withRouter, // Without this, yield put(push('/path')) will not cause the <Router> to swap containers withConnect )(App);

app/index.js - activating our sagas

// ... export class App extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function constructor(props) { super(props); window.onGoogleYoloLoad = (api) => { props.dispatch(actions.googleYoloLoadedAction(api)); }; } componentDidMount() { this.props.dispatch(actions.appStartAction()); } // ... }

saga.googleYolo.js

import { all, take, takeEvery, call, put, select, race } from 'redux-saga/effects'; import * as actions from './actions'; import * as apiActions from './actions.api'; import { makeSelectGoogleYoloApi } from './selectors'; import { // ... } from './constants'; import { // ... } from './constants.api'; const getGoogleYoloApi = makeSelectGoogleYoloApi(); // Root saga // single entry point to start all Sagas at once export default function* rootSaga() { yield all([ googleAuthWatcher(), yield takeEvery(GOOGLE_YOLO_HINT, handleGoogleYoloHint), yield takeEvery(GOOGLE_YOLO_RETRIEVE_CREDENTIALS_SUCCESS, handleGoogleYoloCredentialsSuccess), yield takeEvery(GOOGLE_YOLO_RETRIEVE_CREDENTIALS_FAILURE, handleGoogleYoloCredentialsFailure), // ... ]); } // ... /** * Saga watchers and workers */ /** * Watcher for the google yolo api loaded and auth token read actions * If there is no auth token, it attempts a google sign in */ function_ googleAuthWatcher() { const [credentialsUnavailableAction, googleYoloAction] = yield all([ // eslint-disable-line no-unused-vars take(APP_START_CREDENTIALS_UNAVAILABLE), take(GOOGLE_YOLO_LOADED), ]); yield handleGoogleAuthWatcher(googleYoloAction); }

saga.js

import { push } from 'react-router-redux'; import { fork, all, race, take, takeEvery, put, select } from 'redux-saga/effects'; import Cookies from 'js-cookie'; import * as actions from './actions'; import * as apiActions from './actions.api'; import { makeSelectCurrentUser } from './selectors'; import { // ... } from './constants'; import { // ... } from './constants.api'; const selectCurrentUser = makeSelectCurrentUser(); /** * Saga watchers and workers */ // Root saga // single entry point to start all Sagas at once export default function* rootSaga() { yield all([ // App watchAppStart(), yield takeEvery(APP_START_CREDENTIALS_LOADED, handleAppStartCredentialsLoadedAction), yield takeEvery(APP_START_CREDENTIALS_UNAVAILABLE, handleAppStartCredentialsUnavailableAction), // Log in / Log out yield takeEvery(USER_LOG_IN_START, handleUserLoginStart), yield takeEvery(USER_LOG_IN_SUCCESS, handleUserLoginSuccess), yield takeEvery(USER_LOG_IN_FAILURE, handleUserLoginFailure), yield takeEvery(USER_LOG_OUT, handleUserLogout), // ... ]); } // ... /** * App */ /** * Watcher for the app start */ function_ watchAppStart() { yield takeEvery(APP_START, handleAppStart); } /** * Worker for app start */ function_ handleAppStart() { yield put(push('/')); // Fire boot tasks async as much as possible yield fork(appStartAuth); } /** * Handles the auth start boot check */ function_ appStartAuth() { // Check auth cookies const credentials = Cookies.getJSON(AUTH_COOKIE_NAME); if (credentials) { yield put(actions.appStartCredentialsLoadedAction(credentials)); return; } // If there's no refresh token, it's over const refreshToken = Cookies.get(REFRESH_TOKEN_COOKIE_NAME); if (!refreshToken) { yield put(actions.appStartCredentialsUnavailableAction()); return; } // ... } /** * Handles the auth start boot check */ function_ handleAppStartCredentialsUnavailableAction() { yield put(push('/auth')); }

saga.googleYolo.js

// ... /** * Calls the google yolo api to cancel the retrieve credentials * * @param googleYolo * @returns {Promise|Promise.<T>|*} */ function callGoogleYoloRetrieve(googleYolo) { return googleYolo.retrieve(googleYoloOptions) .then((response) => ({ response })) .catch((error) => ({ error })); } // ... /** * Google yolo api watcher handler - fires the aysnc request for credentials */ function_ handleGoogleAuthWatcher(action) { yield put({ type: GOOGLE_YOLO_RETRIEVE_CREDENTIALS_START }); const { response, error } = yield call(callGoogleYoloRetrieve, action.api); // ... }

saga.api.js

import { all, takeLatest, call, put, select, race, take, } from "redux-saga/effects"; import deepmerge from "deepmerge"; import Cookies from "js-cookie"; import * as apiActions from "./actions.api"; import { makeSelectAuth, makeSelectRefreshToken } from "./selectors"; import * as constants from "./constants"; import * as apiConstants from "./constants.api"; const selectAuth = makeSelectAuth(); const selectAuthRefreshToken = makeSelectRefreshToken(); // ... // Root saga // single entry point to start all Sagas at once export default function* rootSaga() { yield all([ // Boot yield takeLatest( constants.APP_START_CREDENTIALS_LOADED, handleAppStartCredentialsLoaded ), // Auth yield takeLatest( apiConstants.API_AUTH_CREDENTIALS_REQUEST, handleApiAuthCredentialsRequest ), yield takeLatest( apiConstants.API_AUTH_GOOGLE_REQUEST, handleApiAuthGoogleRequest ), yield takeLatest( apiConstants.API_AUTH_REFRESH_REQUEST, handleApiAuthRefreshRequest ), yield takeLatest( apiConstants.API_IDENTITY_REQUEST, handleApiIdentityRequest ), // User yield takeLatest(apiConstants.API_USER_REQUEST, handleApiUserRequest), yield takeLatest( apiConstants.API_USER_DELETE_REQUEST, handleApiUserDeleteRequest ), ]); } // .. function* handleApiAuthGoogleRequest(action) { const endpoint = `${API_URL}/v1/authGoogle`; const body = JSON.stringify({ grant_type: "password", scope: "", // This is all stub for now anyway client: `${CLIENT_TYPE}+${API_VERSION}`, credentials: action.credentials, }); const mergedOptions = Object.assign({}, baseOptions, { method: "POST", body, }); try { const credentials = yield call(request, undefined, endpoint, mergedOptions); yield updateAuthCredentials(credentials); yield put(apiActions.apiAuthGoogleSuccessAction(credentials)); } catch (error) { yield put(apiActions.apiAuthGoogleFailureAction(error)); } }

saga.googleYolo.js

// ... /** * Google yolo api watcher handler - fires the aysnc request for credentials */ function* handleGoogleAuthWatcher(action) { // ... const { response, error } = yield call(callGoogleYoloRetrieve, action.api); const { response, error } = yield call(callGoogleYoloRetrieve, action.api); if (response) { yield put({ type: GOOGLE_YOLO_RETRIEVE_CREDENTIALS_SUCCESS, credentials: response, }); } else { yield put({ type: GOOGLE_YOLO_RETRIEVE_CREDENTIALS_FAILURE, error }); } }

saga.api.js

// ... /** * Worker for user login success */ function* handleUserLoginSuccess() { yield put(push("/profile")); }

Full code examples

actions.api.js

/** * Api actions */ import * as constants from "./constants.api"; // POST auth export function apiAuthCredentialsStartAction(credentials) { return { type: constants.API_AUTH_CREDENTIALS_REQUEST_SUCCESS, credentials }; } export function apiAuthCredentialsSuccessAction(credentials) { return { type: constants.API_AUTH_CREDENTIALS_REQUEST_SUCCESS, credentials }; } export function apiAuthCredentialsFailureAction(error) { return { type: constants.API_AUTH_CREDENTIALS_REQUEST_FAILURE, error }; } // POST authGoogle export function apiAuthGoogleStartAction(googleCredentials) { return { type: constants.API_AUTH_GOOGLE_REQUEST, credentials: googleCredentials, }; } export function apiAuthGoogleSuccessAction(credentials) { return { type: constants.API_AUTH_GOOGLE_REQUEST_SUCCESS, credentials }; } export function apiAuthGoogleFailureAction(error) { return { type: constants.API_AUTH_GOOGLE_REQUEST_FAILURE, error }; } // POST authRefresh export function apiAuthRefreshStartAction(refreshToken) { return { type: constants.API_AUTH_REFRESH_REQUEST, refreshToken }; } export function apiAuthRefreshSuccessAction(credentials) { return { type: constants.API_AUTH_REFRESH_REQUEST_SUCCESS, credentials }; } export function apiAuthRefreshFailureAction(error) { return { type: constants.API_AUTH_REFRESH_REQUEST_FAILURE, error }; } // Generic action for auth updates export function authUpdatedAction(credentials, refreshToken) { return { type: constants.API_AUTH_UPDATED, credentials, refreshToken }; } // GET auth export function apiIdentityRequestAction() { return { type: constants.API_IDENTITY_REQUEST }; } export function apiIdentityRequestSuccessAction(identity) { return { type: constants.API_IDENTITY_REQUEST_SUCCESS, identity }; } export function apiIdentityRequestFailureAction(error) { return { type: constants.API_IDENTITY_REQUEST_FAILURE, error }; } // GET user export function apiUserRequestAction(options) { return { type: constants.API_USER_REQUEST, options }; } export function apiUserRequestSuccessAction(user) { return { type: constants.API_USER_REQUEST_SUCCESS, user }; } export function apiUserRequestFailureAction(error) { return { type: constants.API_USER_REQUEST_FAILURE, error }; } // DELETE user export function apiUserDeleteRequestAction(username) { return { type: constants.API_USER_DELETE_REQUEST, username }; } export function apiUserDeleteRequestSuccessAction() { return { type: constants.API_USER_DELETE_REQUEST_SUCCESS }; } export function apiUserDeleteRequestFailureAction(error) { return { type: constants.API_USER_DELETE_REQUEST_FAILURE, error }; }

actions.js

/** * App actions */ import * as constants from "./constants"; /** * App */ export function appStartAction() { return { type: constants.APP_START }; } export function appStartCredentialsLoadedAction(credentials) { return { type: constants.APP_START_CREDENTIALS_LOADED, credentials }; } export function appStartCredentialsUnavailableAction() { return { type: constants.APP_START_CREDENTIALS_UNAVAILABLE }; } /** * Google */ /** * @param {Object} api * @returns {{type, api: *}} */ export function googleYoloLoadedAction(api) { return { type: constants.GOOGLE_YOLO_LOADED, api }; } export function googleYoloHintAction() { return { type: constants.GOOGLE_YOLO_HINT }; } export function googleYoloSignoutAction() { return { type: constants.GOOGLE_YOLO_SIGN_OUT }; } export function googleYoloSignoutStartAction() { return { type: constants.GOOGLE_YOLO_SIGN_OUT_START }; } export function googleYoloSignoutSuccessAction() { return { type: constants.GOOGLE_YOLO_SIGN_OUT_SUCCESS }; } /** * @param {string} error * @returns {{type, error: *}} */ export function googleYoloSignoutFailureAction(error) { return { type: constants.GOOGLE_YOLO_SIGN_OUT_FAILURE, error }; } /** * User */ export function userLoginStartAction() { return { type: constants.USER_LOG_IN_START }; } export function userLoginSuccessAction(user) { return { type: constants.USER_LOG_IN_SUCCESS, user }; } export function userLoginFailureAction(error) { return { type: constants.USER_LOG_IN_FAILURE, error }; } export function userLogoutAction() { return { type: constants.USER_LOG_OUT }; } export function userLogoutStartAction() { return { type: constants.USER_LOG_OUT_START }; } export function userLogoutSuccessAction() { return { type: constants.USER_LOG_OUT_SUCCESS }; } export function userLogoutFailureAction(error) { return { type: constants.USER_LOG_OUT_FAILURE, error }; } export function userDeleteAction() { return { type: constants.USER_DELETE }; } export function userDeleteStartAction() { return { type: constants.USER_DELETE_START }; } export function userDeleteSuccessAction() { return { type: constants.USER_DELETE_SUCCESS }; } export function userDeleteFailureAction(error) { return { type: constants.USER_DELETE_FAILURE, error }; }

constants.api.js

/* * ApiConstants * Each action has a corresponding type, which the reducer knows and picks up on. * To avoid weird typos between the reducer and the actions, we save them as * constants here. We prefix them with 'yourproject/YourComponent' so we avoid * reducers accidentally picking up actions they shouldn't. * * Follow this format: * export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT'; */ // Auth export const API_AUTH_REFRESH_REQUEST = "api/auth/refresh/request"; export const API_AUTH_REFRESH_REQUEST_SUCCESS = "api/auth/refresh/request/success"; export const API_AUTH_REFRESH_REQUEST_FAILURE = "api/auth/refresh/request/failure"; // Auth export const API_AUTH_CREDENTIALS_REQUEST = "api/authCredentials/request"; export const API_AUTH_CREDENTIALS_REQUEST_SUCCESS = "api/authCredentials/request/success"; export const API_AUTH_CREDENTIALS_REQUEST_FAILURE = "api/authCredentials/request/failure"; export const API_AUTH_GOOGLE_REQUEST = "api/authGoogle/request"; export const API_AUTH_GOOGLE_REQUEST_SUCCESS = "api/authGoogle/request/success"; export const API_AUTH_GOOGLE_REQUEST_FAILURE = "api/authGoogle/request/failure"; export const API_AUTH_UPDATED = "api/auth/updated"; export const API_IDENTITY_REQUEST = "api/identity/request"; export const API_IDENTITY_REQUEST_SUCCESS = "api/identity/request/success"; export const API_IDENTITY_REQUEST_FAILURE = "api/identity/request/failure"; // User export const API_USER_REQUEST = "api/user/request"; export const API_USER_REQUEST_SUCCESS = "api/user/request/success"; export const API_USER_REQUEST_FAILURE = "api/user/request/failure"; export const API_USER_DELETE_REQUEST = "api/user/delete/request"; export const API_USER_DELETE_REQUEST_SUCCESS = "api/user/delete/request/success"; export const API_USER_DELETE_REQUEST_FAILURE = "api/user/delete/request/failure";

constants.js

/* * AppConstants * Each action has a corresponding type, which the reducer knows and picks up on. * To avoid weird typos between the reducer and the actions, we save them as * constants here. We prefix them with 'yourproject/YourComponent' so we avoid * reducers accidentally picking up actions they shouldn't. * * Follow this format: * export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT'; */ export const DEFAULT_LOCALE = "en"; export const AUTH_COOKIE_NAME = "auth_credentials"; export const REFRESH_TOKEN_COOKIE_NAME = "refresh_token"; // App start export const APP_START = "app/start"; export const APP_START_CREDENTIALS_LOADED = "app/start/credentials_loaded"; export const APP_START_CREDENTIALS_UNAVAILABLE = "app/start/credentials_unavailable"; // User export const USER_LOG_IN = "user/log_in"; export const USER_LOG_IN_START = "user/log_in_start"; export const USER_LOG_IN_SUCCESS = "user/log_in_success"; export const USER_LOG_IN_FAILURE = "user/log_in_failure"; export const USER_LOG_OUT = "user/log_out"; export const USER_LOG_OUT_START = "user/log_out_start"; export const USER_LOG_OUT_SUCCESS = "user/log_out_success"; export const USER_LOG_OUT_FAILURE = "user/log_out_failure"; export const USER_DELETE = "user/delete"; export const USER_DELETE_START = "user/delete_start"; export const USER_DELETE_SUCCESS = "user/delete_success"; export const USER_DELETE_FAILURE = "user/delete_failure"; // Google integration export const GOOGLE_APP_ID = "6791872724-gj613j9734j38bu41i6j5idlvbba7529.apps.googleusercontent.com"; export const GOOGLE_YOLO_LOADED = "google/yolo_loaded"; export const GOOGLE_YOLO_HINT = "google/yolo_hint"; export const GOOGLE_YOLO_RETRIEVE_CREDENTIALS_START = "google/yolo_retrieve_credentials_start"; export const GOOGLE_YOLO_RETRIEVE_CREDENTIALS_SUCCESS = "google/yolo_retrieve_credentials_success"; // export const GOOGLE_YOLO_RETRIEVE_CREDENTIALS_TOKEN = 'google/yolo_retrieve_credentials_token'; // export const GOOGLE_YOLO_RETRIEVE_CREDENTIALS_PASSWORD = 'google/yolo_retrieve_credentials_password'; export const GOOGLE_YOLO_RETRIEVE_CREDENTIALS_FAILURE = "google/yolo_retrieve_credentials_failure"; export const GOOGLE_YOLO_SIGN_OUT = "google/yolo_signout"; export const GOOGLE_YOLO_SIGN_OUT_START = "google/yolo_sign_out_start"; export const GOOGLE_YOLO_SIGN_OUT_SUCCESS = "google/yolo_logout_success"; export const GOOGLE_YOLO_SIGN_OUT_FAILURE = "google/yolo_logout_failure"; export const GOOGLE_YOLO_CANCEL = "google/yolo_cancel";

index.js

/** * * App.js * * This component is the skeleton around the actual pages, and should only * contain code that should be seen on all pages. (e.g. navigation bar) * * NOTE: while this component should technically be a stateless functional * component (SFC), hot reloading does not currently support SFCs. If hot * reloading is not a necessity for you then you can refactor it and remove * the linting exception. */ import { compose } from "redux"; import React from "react"; import PropTypes from "prop-types"; import injectSaga from "utils/injectSaga"; import injectReducer from "utils/injectReducer"; import { connect } from "react-redux"; import { withRouter, Switch, Route } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import HomePage from "containers/HomePage/Loadable"; import AuthPage from "containers/AuthPage/Loadable"; import ProfilePage from "containers/ProfilePage/Loadable"; import NotFoundPage from "containers/NotFoundPage/Loadable"; import { makeSelectApp } from "./selectors"; import * as actions from "./actions"; import appSaga from "./saga"; import appReducer from "./reducer"; import googleYoloSaga from "./saga.googleYolo"; import googleYoloReducer from "./reducer.googleYolo"; import apiSaga from "./saga.api"; import apiReducer from "./reducer.api"; export class App extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function constructor(props) { super(props); window.onGoogleYoloLoad = (api) => { props.dispatch(actions.googleYoloLoadedAction(api)); }; } componentDidMount() { this.props.dispatch(actions.appStartAction()); } render() { return ( <div> <Switch> <Route exact path="/" component={HomePage} /> <Route exact path="/auth" component={AuthPage} /> <Route exact path="/profile" component={ProfilePage} /> <Route component={NotFoundPage} /> </Switch> </div> ); } } App.propTypes = { dispatch: PropTypes.func.isRequired, }; const mapStateToProps = createStructuredSelector({ app: makeSelectApp, }); function mapDispatchToProps(dispatch) { return { dispatch, }; } const withConnect = connect(mapStateToProps, mapDispatchToProps); const withAppReducer = injectReducer({ key: "app", reducer: appReducer }); const withAppSaga = injectSaga({ key: "app", saga: appSaga }); const withGoogleYoloReducer = injectReducer({ key: "googleYolo", reducer: googleYoloReducer, }); const withGoogleYoloSaga = injectSaga({ key: "googleYolo", saga: googleYoloSaga, }); const withApiReducer = injectReducer({ key: "api", reducer: apiReducer }); const withApiSaga = injectSaga({ key: "api", saga: apiSaga }); export default compose( withAppReducer, withAppSaga, withGoogleYoloReducer, withGoogleYoloSaga, withApiReducer, withApiSaga, withRouter, withConnect )(App);

reducer.api.js

/** * AuthPage reducer */ import { fromJS } from "immutable"; import { API_AUTH_UPDATED } from "./constants.api"; const initialState = fromJS({ auth: undefined, refreshToken: undefined, }); function apiReducer(state = initialState, action) { switch (action.type) { // Auth case API_AUTH_UPDATED: return Object.assign({}, state, { auth: action.credentials, refreshToken: action.refreshToken, }); default: return state; } } export default apiReducer;

reducer.googleYolo.js

/* * * AuthPage reducer * */ import { fromJS } from "immutable"; import { GOOGLE_YOLO_LOADED, GOOGLE_YOLO_RETRIEVE_CREDENTIALS_SUCCESS, GOOGLE_YOLO_SIGN_OUT_SUCCESS, } from "./constants"; const initialState = fromJS({ api: undefined, credentials: undefined, }); function googleYoloReducer(state = initialState, action) { switch (action.type) { case GOOGLE_YOLO_LOADED: return Object.assign({}, state, { api: action.api, }); case GOOGLE_YOLO_RETRIEVE_CREDENTIALS_SUCCESS: return Object.assign({}, state, { credentials: action.credentials, }); case GOOGLE_YOLO_SIGN_OUT_SUCCESS: return Object.assign({}, state, { credentials: undefined, }); default: return state; } } export default googleYoloReducer;

reducer.js

/** * AuthPage reducer */ import { fromJS } from "immutable"; import { API_IDENTITY_REQUEST_SUCCESS } from "./constants.api"; import { USER_LOG_OUT_SUCCESS } from "./constants"; const initialState = fromJS({ currentUser: undefined, }); function appReducer(state = initialState, action) { switch (action.type) { case API_IDENTITY_REQUEST_SUCCESS: return Object.assign({}, state, { currentUser: action.identity, }); case USER_LOG_OUT_SUCCESS: return Object.assign({}, state, { currentUser: undefined, }); default: return state; } } export default appReducer;

saga.api.js

import { all, takeLatest, call, put, select, race, take, } from "redux-saga/effects"; import deepmerge from "deepmerge"; import Cookies from "js-cookie"; import * as apiActions from "./actions.api"; import { makeSelectAuth, makeSelectRefreshToken } from "./selectors"; import * as constants from "./constants"; import * as apiConstants from "./constants.api"; const selectAuth = makeSelectAuth(); const selectAuthRefreshToken = makeSelectRefreshToken(); const CLIENT_TYPE = "react"; const API_VERSION = "0.1.0"; const baseOptions = { method: "GET", headers: { "Content-Type": "application/json" }, }; // const CLIENT_TYPE = 'react'; // const API_VERSION = '0.1.0'; const API_URL = process.env.NODE_ENV === "production" ? "https://services.lookingforgroup.com" : "http://localhost:10010"; // Root saga // single entry point to start all Sagas at once export default function* rootSaga() { yield all([ // Boot yield takeLatest( constants.APP_START_CREDENTIALS_LOADED, handleAppStartCredentialsLoaded ), // Auth yield takeLatest( apiConstants.API_AUTH_CREDENTIALS_REQUEST, handleApiAuthCredentialsRequest ), yield takeLatest( apiConstants.API_AUTH_GOOGLE_REQUEST, handleApiAuthGoogleRequest ), yield takeLatest( apiConstants.API_AUTH_REFRESH_REQUEST, handleApiAuthRefreshRequest ), yield takeLatest( apiConstants.API_IDENTITY_REQUEST, handleApiIdentityRequest ), // User yield takeLatest(apiConstants.API_USER_REQUEST, handleApiUserRequest), yield takeLatest( apiConstants.API_USER_DELETE_REQUEST, handleApiUserDeleteRequest ), ]); } function* handleAppStartCredentialsLoaded(action) { yield updateAuthCredentials(action.credentials); } // POST /auth function* handleApiAuthCredentialsRequest(action) { const endpoint = `${API_URL}/v1/auth`; const body = JSON.stringify({ grant_type: "password", scope: "", // This is all stub for now anyway client: `${CLIENT_TYPE}+${API_VERSION}`, username: action.username, password: action.password, }); const mergedOptions = Object.assign({}, baseOptions, { method: "POST", body, }); try { const credentials = yield call(request, undefined, endpoint, mergedOptions); yield updateAuthCredentials(credentials); yield put(apiActions.apiAuthCredentialsSuccessAction(credentials)); } catch (error) { yield put(apiActions.apiAuthCredentialsFailureAction(error)); } } // POST /authGoogle function* handleApiAuthGoogleRequest(action) { const endpoint = `${API_URL}/v1/authGoogle`; const body = JSON.stringify({ grant_type: "password", scope: "", // This is all stub for now anyway client: `${CLIENT_TYPE}+${API_VERSION}`, credentials: action.credentials, }); const mergedOptions = Object.assign({}, baseOptions, { method: "POST", body, }); try { const credentials = yield call(request, undefined, endpoint, mergedOptions); yield updateAuthCredentials(credentials); yield put(apiActions.apiAuthGoogleSuccessAction(credentials)); } catch (error) { yield put(apiActions.apiAuthGoogleFailureAction(error)); } } // POST /authRefresh function* handleApiAuthRefreshRequest(action) { const endpoint = `${API_URL}/v1/authRefresh`; const body = JSON.stringify({ grant_type: "refresh_token", // scope: '', // This is all stub for now anyway, but I think I'd want the same scope client: `${CLIENT_TYPE}+${API_VERSION}`, refresh_token: action.refreshToken, }); const mergedOptions = Object.assign({}, baseOptions, { method: "POST", body, }); try { const credentials = yield call(request, undefined, endpoint, mergedOptions); yield updateAuthCredentials(credentials); yield put(apiActions.apiAuthRefreshSuccessAction(credentials)); } catch (error) { yield put(apiActions.apiAuthRefreshFailureAction(error)); } } /** * @param {object} credentials * @return boolean */ function* updateAuthCredentials(credentials) { // Determine the token expiration timestamp const credentialsExpiresDate = new Date( new Date().getTime() + credentials.expires_in * 1000 ); const mergedCredentials = Object.assign({}, credentials, { expires_at: credentialsExpiresDate, }); // Set a cookie for the credentials according auth_token expires Cookies.set(constants.AUTH_COOKIE_NAME, mergedCredentials, { expires: credentialsExpiresDate, }); // Store the refresh token on a more permanent basis const refreshExpiresDate = new Date(new Date().getTime()); refreshExpiresDate.setDate(refreshExpiresDate.getDate() + 14); Cookies.set(constants.REFRESH_TOKEN_COOKIE_NAME, credentials.refresh_token, { expires: refreshExpiresDate, }); // This triggers the reducers to update the api.* state yield put( apiActions.authUpdatedAction( mergedCredentials, mergedCredentials.refresh_token ) ); } // GET /auth function* handleApiIdentityRequest() { const endpoint = `${API_URL}/v1/auth`; const mergedOptions = Object.assign({}, baseOptions, { method: "GET" }); const accessToken = yield getAccessToken(); try { // An alias for the call [obj, obj.method] version const identity = yield call(request, accessToken, endpoint, mergedOptions); yield put(apiActions.apiIdentityRequestSuccessAction(identity)); } catch (error) { yield put(apiActions.apiIdentityRequestFailureAction(error)); } } // GET /user function* handleApiUserRequest(action) { console.warn("TODO", action); } // DELETE /user function* handleApiUserDeleteRequest(action) { const endpoint = `${API_URL}/v1/user/?username=${action.username}`; const mergedOptions = Object.assign({}, baseOptions, { method: "DELETE" }); const accessToken = yield getAccessToken(); try { // An alias for the call [obj, obj.method] version const identity = yield call(request, accessToken, endpoint, mergedOptions); yield put(apiActions.apiUserDeleteRequestSuccessAction(identity)); } catch (error) { yield put(apiActions.apiUserDeleteRequestFailureAction(error)); } } /** * Gets the credentials for the api. * If expired, the token is refreshed. */ function* getAccessToken() { const credentials = yield select(selectAuth); if (!credentials) { return undefined; } // Is it still active? const expirationDate = new Date(credentials.expires_at); if (expirationDate > Date.now()) { return credentials.access_token; } // Attempt refresh let refreshToken = yield select(selectAuthRefreshToken); if (!refreshToken) { return undefined; } // Attempt a refresh yield put(apiActions.apiAuthRefreshStartAction()); const { refreshAction, errorAction } = yield race({ refreshAction: take(apiConstants.API_AUTH_REFRESH_REQUEST_SUCCESS), errorAction: take(apiConstants.API_AUTH_REFRESH_REQUEST_FAILURE), }); if (errorAction) { yield put(apiActions.apiAuthRefreshFailureAction(errorAction)); refreshToken = undefined; } if (refreshAction) { yield put(apiActions.apiAuthRefreshSuccessAction(refreshAction)); refreshToken = yield select(selectAuthRefreshToken); } return refreshToken; } /** * Attaches the auth accessToken to requests if defined * @param {string} accessToken * @param {object} options * @returns {*} */ function addAccessToken(accessToken, options) { if (!accessToken) { return options; } const authAddition = { headers: { Authorization: `Bearer ${accessToken}`, }, }; return deepmerge(authAddition, options); } /** * @param {String} url * @param {Object} options */ function addQueryParameters(url, options) { if (!options.query) { return url; } const urlParams = new URLSearchParams(Object.entries(options.query)); return url + urlParams.toString(); } /** * Requests a URL, returning a promise * * @param {string|undefined} accessToken * @param {string} url The URL we want to request * @param {object} [options] The options we want to pass to "fetch" * @return Promise */ function request(accessToken, url, options) { const mergedUrl = addQueryParameters(url, options); const mergedOptions = addAccessToken(accessToken, options); console.log(mergedUrl, mergedOptions); return fetch(mergedUrl, mergedOptions) .then((response) => response.json().then((data) => ({ response, data }))) .then((args) => { const { response, data } = args; if (!response.ok) { const message = data.message || response.statusText; throw new Error(`${response.status}: ${message}`); } return data; }); }

saga.googleYolo.js

// https://developers.google.com/identity/one-tap/web/retrieve-credentials // https://developers.google.com/identity/one-tap/web/retrieve-hints import { all, take, takeEvery, call, put, select, race, } from "redux-saga/effects"; import * as actions from "./actions"; import * as apiActions from "./actions.api"; import { makeSelectGoogleYoloApi } from "./selectors"; import { GOOGLE_APP_ID, GOOGLE_YOLO_LOADED, GOOGLE_YOLO_RETRIEVE_CREDENTIALS_START, GOOGLE_YOLO_RETRIEVE_CREDENTIALS_SUCCESS, GOOGLE_YOLO_RETRIEVE_CREDENTIALS_FAILURE, // GOOGLE_YOLO_RETRIEVE_CREDENTIALS_TOKEN, // GOOGLE_YOLO_RETRIEVE_CREDENTIALS_PASSWORD, GOOGLE_YOLO_HINT, GOOGLE_YOLO_SIGN_OUT, // GOOGLE_YOLO_SIGN_OUT_FAILURE, // GOOGLE_YOLO_SIGN_OUT_SUCCESS, GOOGLE_YOLO_CANCEL, APP_START_CREDENTIALS_UNAVAILABLE, } from "./constants"; import { API_AUTH_CREDENTIALS_REQUEST_SUCCESS, API_AUTH_CREDENTIALS_REQUEST_FAILURE, API_AUTH_GOOGLE_REQUEST_SUCCESS, API_AUTH_GOOGLE_REQUEST_FAILURE, API_IDENTITY_REQUEST_SUCCESS, API_IDENTITY_REQUEST_FAILURE, } from "./constants.api"; const getGoogleYoloApi = makeSelectGoogleYoloApi(); // Root saga // single entry point to start all Sagas at once export default function* rootSaga() { yield all([ googleAuthWatcher(), yield takeEvery(GOOGLE_YOLO_HINT, handleGoogleYoloHint), yield takeEvery( GOOGLE_YOLO_RETRIEVE_CREDENTIALS_SUCCESS, handleGoogleYoloCredentialsSuccess ), yield takeEvery( GOOGLE_YOLO_RETRIEVE_CREDENTIALS_FAILURE, handleGoogleYoloCredentialsFailure ), yield takeEvery(GOOGLE_YOLO_CANCEL, handleGoogleYoloCancel), yield takeEvery(GOOGLE_YOLO_SIGN_OUT, handleGoogleYoloSignout), ]); } /** * Google api setup */ const googleYoloOptions = { supportedAuthMethods: [ "https://accounts.google.com", "googleyolo://id-and-password", ], supportedIdTokenProviders: [ { uri: "https://accounts.google.com", clientId: GOOGLE_APP_ID, }, ], }; /** * Calls the google yolo api to cancel the retrieve credentials * * @param googleYolo * @returns {Promise|Promise.<T>|*} */ function callGoogleYoloRetrieve(googleYolo) { return googleYolo .retrieve(googleYoloOptions) .then((response) => ({ response })) .catch((error) => ({ error })); } /** * Calls the google yolo api suggest users should log in with their google account * * @param googleYolo * @returns {Promise|Promise.<T>|*} */ function callGoogleYoloHint(googleYolo) { return googleYolo .hint(googleYoloOptions) .then((response) => ({ response })) .catch((error) => ({ error })); } /** * Calls the google yolo api to cancel the previous operation * * @param googleYolo * @returns {Promise|Promise.<T>|*} */ function callGoogleYoloCancel(googleYolo) { return googleYolo .cancelLastOperation() .then((response) => ({ response })) .catch((error) => ({ error })); } /** * Calls the google yolo api to disable the automatic login * * @param googleYolo * @returns {Promise|Promise.<T>|*} */ function callGoogleYoloDisableAutoLogin(googleYolo) { return googleYolo .disableAutoSignIn() .catch((error) => ({ error })) .then(() => ({ response: true })); // disableAutoSignIn returns undefined } /** * Saga watchers and workers */ /** * Watcher for the google yolo api loaded and auth token read actions * If there is no auth token, it attempts a google sign in */ function* googleAuthWatcher() { const [credentialsUnavailableAction, googleYoloAction] = yield all([ // eslint-disable-line no-unused-vars take(APP_START_CREDENTIALS_UNAVAILABLE), take(GOOGLE_YOLO_LOADED), ]); yield handleGoogleAuthWatcher(googleYoloAction); } /** * Google yolo api watcher handler - fires the aysnc request for credentials */ function* handleGoogleAuthWatcher(action) { yield put({ type: GOOGLE_YOLO_RETRIEVE_CREDENTIALS_START }); const { response, error } = yield call(callGoogleYoloRetrieve, action.api); if (response) { yield put({ type: GOOGLE_YOLO_RETRIEVE_CREDENTIALS_SUCCESS, credentials: response, }); } else { yield put({ type: GOOGLE_YOLO_RETRIEVE_CREDENTIALS_FAILURE, error }); } } /** * Worker function for credentials retrieval * @param {object} action */ function* handleGoogleYoloCredentialsSuccess(action) { if (action.credentials.password) { yield logInWithGoogleAccountCredentials( action.credentials.username, action.credentials.password ); } else { yield logInWithGoogleTokenCredentials(action.credentials); } const { identityAction, errorAction } = yield race({ identityAction: take(API_IDENTITY_REQUEST_SUCCESS), errorAction: take(API_IDENTITY_REQUEST_FAILURE), }); if (errorAction) { yield put(actions.userLoginFailureAction()); } if (identityAction) { yield put(actions.userLoginSuccessAction(identityAction.identity)); } } /** * @param {String} username * @param {String} password */ function* logInWithGoogleAccountCredentials(username, password) { yield put(apiActions.apiAuthCredentialsStartAction(username, password)); const { authAction, errorAction } = yield race({ authAction: take(API_AUTH_CREDENTIALS_REQUEST_SUCCESS), errorAction: take(API_AUTH_CREDENTIALS_REQUEST_FAILURE), }); if (errorAction) { yield put(actions.appStartCredentialsUnavailableAction()); return; } if (authAction) { yield put(actions.appStartCredentialsLoadedAction(authAction.credentials)); } } /** * @param {Object} googleCredentials */ function* logInWithGoogleTokenCredentials(googleCredentials) { yield put(apiActions.apiAuthGoogleStartAction(googleCredentials)); const { authAction, errorAction } = yield race({ authAction: take(API_AUTH_GOOGLE_REQUEST_SUCCESS), errorAction: take(API_AUTH_GOOGLE_REQUEST_FAILURE), }); if (errorAction) { yield put(actions.appStartCredentialsUnavailableAction()); return; } if (authAction) { yield put(actions.appStartCredentialsLoadedAction(authAction.credentials)); } } /** * Worker function for handling retrieve credentials failures * * Credentials could not be retrieved. In general, if the user does not need to be signed in to use the page, you can * just fail silently; or, you can also examine the error object to handle specific error cases. */ function* handleGoogleYoloHint() { const googleYolo = yield select(getGoogleYoloApi); const { response, error } = yield call(callGoogleYoloHint, googleYolo); console.log("hint result:", response, error); if (response) { yield put({ type: GOOGLE_YOLO_RETRIEVE_CREDENTIALS_SUCCESS, credentials: response, }); } } /** * Worker function for handling retrieve credentials failures * * Credentials could not be retrieved. In general, if the user does not need to be signed in to use the page, you can * just fail silently; or, you can also examine the error object to handle specific error cases. * * @param {object} action */ function* handleGoogleYoloCredentialsFailure(action) { if (action.error.type !== "noCredentialsAvailable") { return; } // If retrieval failed because there were no credentials available, and // signing in might be useful or is required to proceed from this page, // you can call `hint()` to prompt the user to select an account to sign // in or sign up with. // yield put({ type: GOOGLE_YOLO_HINT_START }); const googleYolo = yield select(getGoogleYoloApi); const { data } = yield call(callGoogleYoloHint, googleYolo); if (!data) { yield put(actions.userLoginFailureAction()); return; } yield put({ type: GOOGLE_YOLO_RETRIEVE_CREDENTIALS_SUCCESS, data }); } /** * Google yolo cancel worker */ function* handleGoogleYoloCancel() { const googleYolo = yield select(getGoogleYoloApi); const { data } = yield call(callGoogleYoloCancel, googleYolo); if (data) { // yield put({ type: GOOGLE_YOLO_CANCEL_SUCCESS, data }); } else { // yield put({ type: GOOGLE_YOLO_CANCEL_FAILURE, error }); } } /** * Google logout worker */ function* handleGoogleYoloSignout() { yield put(actions.googleYoloSignoutStartAction()); const googleYolo = yield select(getGoogleYoloApi); const { response, error } = yield call( callGoogleYoloDisableAutoLogin, googleYolo ); if (response) { yield put(actions.googleYoloSignoutSuccessAction()); } else { yield put(actions.googleYoloSignoutFailureAction(error)); } }

saga.js

import { push } from "react-router-redux"; import { fork, all, race, take, takeEvery, put, select, } from "redux-saga/effects"; import Cookies from "js-cookie"; import * as actions from "./actions"; import * as apiActions from "./actions.api"; import { makeSelectCurrentUser } from "./selectors"; import { AUTH_COOKIE_NAME, REFRESH_TOKEN_COOKIE_NAME, APP_START, APP_START_CREDENTIALS_UNAVAILABLE, // USER_LOG_IN, // The google auth saga can bypass this USER_LOG_IN_START, USER_LOG_IN_SUCCESS, USER_LOG_IN_FAILURE, APP_START_CREDENTIALS_LOADED, USER_LOG_OUT, USER_LOG_OUT_START, USER_LOG_OUT_SUCCESS, USER_LOG_OUT_FAILURE, // Delete USER_DELETE, USER_DELETE_START, USER_DELETE_SUCCESS, USER_DELETE_FAILURE, } from "./constants"; import { API_AUTH_UPDATED, API_AUTH_REFRESH_REQUEST_SUCCESS, API_AUTH_REFRESH_REQUEST_FAILURE, API_IDENTITY_REQUEST_FAILURE, API_IDENTITY_REQUEST_SUCCESS, API_USER_DELETE_REQUEST_SUCCESS, API_USER_DELETE_REQUEST_FAILURE, } from "./constants.api"; const selectCurrentUser = makeSelectCurrentUser(); /** * Saga watchers and workers */ // Root saga // single entry point to start all Sagas at once export default function* rootSaga() { yield all([ // App watchAppStart(), yield takeEvery( APP_START_CREDENTIALS_LOADED, handleAppStartCredentialsLoadedAction ), yield takeEvery( APP_START_CREDENTIALS_UNAVAILABLE, handleAppStartCredentialsUnavailableAction ), // Log in / Log out yield takeEvery(USER_LOG_IN_START, handleUserLoginStart), yield takeEvery(USER_LOG_IN_SUCCESS, handleUserLoginSuccess), yield takeEvery(USER_LOG_IN_FAILURE, handleUserLoginFailure), yield takeEvery(USER_LOG_OUT, handleUserLogout), yield takeEvery(USER_LOG_OUT_START, handleUserLogoutStart), yield takeEvery(USER_LOG_OUT_SUCCESS, handleUserLogoutSuccess), yield takeEvery(USER_LOG_OUT_FAILURE, handleUserLogoutFailure), // Delete yield takeEvery(USER_DELETE, handleUserDelete), yield takeEvery(USER_DELETE_START, handleUserDeleteStart), yield takeEvery(USER_DELETE_SUCCESS, handleUserDeleteSuccess), yield takeEvery(USER_DELETE_FAILURE, handleUserDeleteFailure), ]); } /** * App */ /** * Watcher for the app start */ function* watchAppStart() { yield takeEvery(APP_START, handleAppStart); } /** * Worker for app start */ function* handleAppStart() { yield put(push("/")); // Fire boot tasks async as much as possible yield fork(appStartAuth); } /** * Handles the auth start boot check */ function* appStartAuth() { // Check auth cookies const credentials = Cookies.getJSON(AUTH_COOKIE_NAME); if (credentials) { yield put(actions.appStartCredentialsLoadedAction(credentials)); return; } // If there's no refresh token, it's over const refreshToken = Cookies.get(REFRESH_TOKEN_COOKIE_NAME); if (!refreshToken) { yield put(actions.appStartCredentialsUnavailableAction()); return; } yield put(apiActions.apiAuthRefreshStartAction()); const { refreshAction, errorAction } = yield race({ refreshAction: take(API_AUTH_REFRESH_REQUEST_SUCCESS), errorAction: take(API_AUTH_REFRESH_REQUEST_FAILURE), }); if (errorAction) { yield put(actions.appStartCredentialsUnavailableAction()); return; } if (refreshAction) { yield put(actions.appStartCredentialsLoadedAction(credentials)); } } /** * Handles the auth start boot check */ function* handleAppStartCredentialsLoadedAction() { // Wait for the api to process the loaded credentials before continuing yield take(API_AUTH_UPDATED); yield put(apiActions.apiIdentityRequestAction()); const { identityAction, errorAction } = yield race({ identityAction: take(API_IDENTITY_REQUEST_SUCCESS), errorAction: take(API_IDENTITY_REQUEST_FAILURE), }); if (errorAction) { yield put(actions.userLoginFailureAction()); } if (identityAction) { yield put(actions.userLoginSuccessAction(identityAction.identity)); } } /** * Handles the auth start boot check */ function* handleAppStartCredentialsUnavailableAction() { yield put(push("/auth")); } /** * Users */ /** * Worker for user log in process - fires the aysnc request for user profile and redirects the user */ function* handleUserLoginStart(action) { console.log("handleUserLoginSuccess", action); } /** * Worker for user login success */ function* handleUserLoginSuccess() { yield put(push("/profile")); } /** * Worker for user login failure */ function* handleUserLoginFailure() { yield put(push("/auth")); } /** * Worker for user log out process */ function* handleUserLogout() { yield put(actions.userLogoutStartAction()); } /** * Worker for user log out start */ function* handleUserLogoutStart() { const user = yield select(selectCurrentUser); // Sign out of third party services if (user && user.googleYolo) { yield put(actions.googleYoloSignoutAction()); } yield put(actions.userLogoutSuccessAction()); } /** * Worker for user log out success */ function* handleUserLogoutSuccess() { // App.currentUser is cleared using reducer at this step Cookies.remove(AUTH_COOKIE_NAME); Cookies.remove(REFRESH_TOKEN_COOKIE_NAME); yield put(push("/auth")); } /** * Worker for user log out failure */ function* handleUserLogoutFailure(action) { console.log("handleUserLoginFailure", action); } /** * Worker for user delete process */ function* handleUserDelete() { yield put(actions.userDeleteStartAction()); } /** * Worker for user delete start */ function* handleUserDeleteStart() { const user = yield select(selectCurrentUser); // Sign out of third party services if (user && user.googleYolo) { yield put(actions.googleYoloSignoutAction()); } yield put(apiActions.apiUserDeleteRequestAction(user.username)); const { deleteAction, errorAction } = yield race({ deleteAction: take(API_USER_DELETE_REQUEST_SUCCESS), errorAction: take(API_USER_DELETE_REQUEST_FAILURE), }); if (errorAction) { yield put(actions.userDeleteFailureAction(errorAction)); } if (deleteAction) { yield put(actions.userDeleteSuccessAction(deleteAction)); } } /** * Worker for user delete success */ function* handleUserDeleteSuccess() { // Clears app.currentUser and redirects to login yield put(actions.userLogoutSuccessAction()); } /** * Worker for user delete failure */ function* handleUserDeleteFailure(action) { console.log("handleUserDeleteFailure", action); }

selectors.js

import { createSelector } from "reselect"; // Route and location const selectRoute = (state) => state.get("route"); export const makeSelectLocation = () => createSelector(selectRoute, (routeState) => routeState.get("location").toJS() ); // App const selectApp = (state) => state.get("app"); export const makeSelectApp = () => createSelector(selectApp, (appState) => appState.get().toJS()); // App.User const selectCurrentUser = (appState) => appState.currentUser; export const makeSelectCurrentUser = () => createSelector(selectApp, (appState) => selectCurrentUser(appState)); // Api const selectApi = (state) => state.get("api"); export const makeSelectApi = () => createSelector(selectApi, (apiState) => apiState.get().toJS()); // Api.auth const selectAuth = (apiState) => apiState.auth; export const makeSelectAuth = () => createSelector(selectApi, (apiState) => selectAuth(apiState)); // Api.refresh_token const selectRefreshToken = (apiState) => apiState.refreshToken; export const makeSelectRefreshToken = () => createSelector(selectApi, (apiState) => selectRefreshToken(apiState)); // GoogleYolo const selectGoogleYolo = (state) => state.get("googleYolo"); export const makeSelectGoogleYolo = () => createSelector(selectGoogleYolo, (googleYoloState) => googleYoloState.get().toJS() ); // GoogleYolo.api const selectGoogleYoloApi = (googleYoloState) => googleYoloState.api; export const makeSelectGoogleYoloApi = () => createSelector(selectGoogleYolo, (googleYoloState) => selectGoogleYoloApi(googleYoloState) ); // GoogleYolo.credentials const selectGoogleYoloCredentials = (googleYoloState) => googleYoloState.credentials; export const makeSelectGoogleYoloCredentials = () => createSelector(selectGoogleYolo, (googleYoloState) => selectGoogleYoloCredentials(googleYoloState) );