Created
January 12, 2018 01:46
-
-
Save lancegliser/af41c0a52e9bd87975286dbfdbdca9a4 to your computer and use it in GitHub Desktop.
An example React Redux Saga set for Google Yolo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 }; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* | |
* 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 }; | |
} | |
/** | |
*/ | |
/** | |
* @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 }; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* | |
* 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* | |
* 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* | |
* 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* | |
* 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment