Skip to content

Instantly share code, notes, and snippets.

@lancegliser
Created January 12, 2018 01:46
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lancegliser/af41c0a52e9bd87975286dbfdbdca9a4 to your computer and use it in GitHub Desktop.
Save lancegliser/af41c0a52e9bd87975286dbfdbdca9a4 to your computer and use it in GitHub Desktop.
An example React Redux Saga set for Google Yolo
/**
* 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 };
}
/*
*
* 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 };
}
/*
* 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';
/*
* 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';
/**
*
* 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);
/*
*
* 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;
/*
*
* 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;
/*
*
* 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;
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;
});
}
// 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));
}
}
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);
}
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