UNPKG

modules-pack

Version:

JavaScript Modules for Modern Frontend & Backend Projects

287 lines (248 loc) 8.6 kB
import { stateAction, stateActionType } from 'modules-pack/redux' import { URL } from 'modules-pack/variables' import { STYLE } from 'react-ui-pack' import { all, call, cancel, cancelled, delay, fork, put, select, take, takeEvery, takeLatest } from 'redux-saga/effects' import { CANCEL, CLOSE, CREATE, enumCheck, ERROR, FINISH, get, isInStringAny, LOGIN, LOGOUT, logTask, OPEN, PREFETCH, REFRESH, REQUEST, RESET, SUCCESS, TIMEOUT, UPDATE } from 'utils-pack' import Api, { apiAction, apiActionType } from '../../actions' import { isApiActionTypeSuccess, resumeActionsPending } from '../../utils' import { ADD_ACTIONS_PENDING_AUTH, API, AUTH, LOGIN_CHECK, } from './constants' import { token as tokenSelector } from './selectors' /** * ASYNC TASKS ================================================================= * Actions Orchestration - for subscribing, managing and dispatching actions. * ============================================================================= */ /* Module's Combined Task to start all tasks at once */ export default function * saga () { yield all([ // List someSaga() here loginWatch(), logoutWatch() // tokenWatch() // only enable if needed, for performance reasons ]) } /** * WATCH TASKS (Action Subscriptions) ------------------------------------------ * ----------------------------------------------------------------------------- */ function * loginWatch () { /* Must takeLatest to ensure login request is always processed * no matter if Logout action was called or not subsequently, * to prevent apiActionsPending from blocking Login */ yield all([ takeLatest([LOGIN_CHECK, ADD_ACTIONS_PENDING_AUTH], loginCheckFlow), takeLatest(stateActionType(API, LOGIN, REQUEST), (action) => loginRequestFlow(action, REQUEST)), takeLatest(stateActionType(API, LOGIN, REFRESH), (action) => loginRequestFlow(action, REFRESH)), takeEvery(stateActionType(API, LOGIN, SUCCESS), loginSuccessFlow) ]) } function * logoutWatch () { /* Take(), followed by async method call, equals takeFirst * because rapid actions are only executed once * since saga has to finish the last yield statement * in order to take the next same action in while loop */ while (true) { yield take(stateActionType(API, LOGOUT)) yield all([ call(Api.clearToken), // Remove token from Storage put(stateAction(RESET)), // Reset the whole app state put(stateAction(API, LOGOUT, FINISH)) // Signal logout complete ]) } } /* Update Token on Success Responses */ function * tokenWatch () { // eslint-disable-line yield all([ takeLatest(isApiActionTypeSuccess, tokenUpdate) ]) } /** * FLOW TASKS (Action Management) ---------------------------------------------- * ----------------------------------------------------------------------------- */ /** * Login Request Actions Flow * * @param {Object} payload - action.payload * @param {String} [actionType] - optional, one of enum values [REQUEST, REFRESH] */ function * loginRequestFlow ({payload}, actionType = REQUEST) { enumCheck([REQUEST, REFRESH], actionType) // Start API Login asynchronously, waiting for response let task if (actionType === REQUEST) { task = yield fork(tokenRequest, URL.LOGIN, payload, {authenticate: false}) } else if (actionType === REFRESH) { task = yield fork(tokenRequest, URL.LOGIN_REFRESH) } // Keep listening for new Actions if Login is interrupted by Logout const {type} = yield take([ stateActionType(API, LOGOUT), stateActionType(API, LOGIN, ERROR) ]) // Cancel Login if Logout if (type === stateActionType(API, LOGOUT)) yield cancel(task) } function * loginSuccessFlow () { /* Signal Prefetching of Resources */ yield put(stateAction(PREFETCH)) /* Resume Actions Pending Authentication */ yield call(resumeActionsPending, AUTH) /* Ensure Token is Still Valid */ const token = yield select(tokenSelector) if (token) { try { /* Close Login View */ yield call(loginViewClose) } catch (e) { // Do nothing } } // eslint-disable-line brace-style /* Token Expired */ else { yield put(stateAction(LOGIN_CHECK)) } } /** * Check if API Token Exists * * * Open Login View if not already open and no token exists * * Dispatch LOGIN SUCCESS on APP Launch if token exists in Storage * * Refresh Token if a token already exists in the state */ function * loginCheckFlow () { // Delay check to ensure all kinds of transition animations have completed const duration = Math.max(STYLE.ANIMATION_DURATION * 3, 300) yield delay(duration) logTask('loginCheckFlow') let token = yield select(tokenSelector) /* APP LAUNCH */ if (!token) { // Because state token is empty -> can only be App Open, or after LOGOUT // try getting token from Storage first // (useful for subsequent App launches where user logged in before) const retrievedToken = yield call(Api.getToken) token = retrievedToken ? String(retrievedToken).replace('Bearer ', '') : null /* LOGIN ALREADY */ if (token) { // Set Token in State yield put(stateAction(API, LOGIN, SUCCESS, {token})) } // eslint-disable-line brace-style /* LOGOUT BEFORE */ else { yield call(loginViewOpen) } } // eslint-disable-line brace-style /* TOKEN EXPIRED */ else { // Because state has token but it's not valid // Refresh Token yield put(stateAction(API, LOGIN, REFRESH)) } } /** * HELPER TASKS (Action Dispatches) -------------------------------------------- * ----------------------------------------------------------------------------- */ /** Open Login View */ function * loginViewOpen () { yield put(stateAction(API, LOGIN, OPEN)) } /** Close Login View */ function * loginViewClose () { yield put(stateAction(API, LOGIN, CLOSE)) } /** * Request Token from API Server * (Used for Initial Login and Token Refresh). * * @param {[string]} URL - API endpoint to fetch * @param {Object} apiPayload - payload to send with apiAction * @param {Object} apiMeta - meta data to send with apiAction */ function * tokenRequest (URL, apiPayload, apiMeta) { try { /* Token Request */ yield put(apiAction(URL, CREATE, apiPayload, { ...apiMeta // ResponseHandler: (response) => { // const headers = response.headers; // console.log('response.headers', headers); // return response; // } })) const {payload = {}, meta = {}} = yield take([ // Take any of these action types apiActionType(URL, CREATE, SUCCESS), apiActionType(URL, CREATE, ERROR), apiActionType(URL, CREATE, TIMEOUT) ]) /* Request Error */ if (meta.result === ERROR) { // Signal Error yield put(stateAction(API, LOGIN, ERROR)) const isRefresh = !apiPayload if (isRefresh) { // Logout yield put(stateAction(API, LOGOUT)) } } // eslint-disable-line brace-style /* Request Timeout */ else if (meta.result === TIMEOUT) { // Signal timeout with message from API Middleware yield put(stateAction(API, LOGIN, TIMEOUT, payload)) } // eslint-disable-line brace-style /* Request Success */ else if (meta.result === SUCCESS) { const token = get(meta, 'headers.token') || get(payload, 'token') || get(payload, 'key') if (token) yield call(Api.storeToken, token) // Set token in state yield put(stateAction(API, LOGIN, SUCCESS, {token, ...payload})) } } // eslint-disable-line brace-style /* Other Errors */ catch (error) { // Signal Error + alert yield put(stateAction(API, LOGIN, ERROR, error)) } // eslint-disable-line brace-style /* Request Canceled */ finally { // Unset loading state without alert, by omitting payload if (yield cancelled()) yield put(stateAction(API, LOGIN, CANCEL)) } } /** * Update Token in Storage and State * (when new token is returned from success responses). */ function * tokenUpdate ({type, payload, meta}) { // Match all Success API Fetch Responses, except from Login endpoints if (isInStringAny(type, URL.LOGIN, URL.LOGIN_REFRESH, URL.LOGOUT)) return const oldToken = yield select(tokenSelector) const token = get(meta, 'headers.token') || get(payload, 'token') || get(payload, 'key') // Update Token If It Has Changed if (token && oldToken && token !== oldToken) { yield call(Api.storeToken, token) yield put(stateAction(API, LOGIN, UPDATE, {token})) } }