UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

498 lines (434 loc) • 14 kB
import * as R from "ramda"; import { subscriber } from "@applicaster/zapp-react-native-utils/functionUtils"; import { QUICK_BRICK_EVENTS, sendQuickBrickEvent, } from "@applicaster/zapp-react-native-bridge/QuickBrick"; import { Hook } from "./Hook"; import { HOOKS_EVENTS, HOOKS_TYPE } from "./constants"; import { hooksManagerLogger } from "./logger"; /** * creates a hooksManager object to handle the hooks process. * it is based on the `subscriber` object * @param {object} options * @param {object} rivers map of rivers in the app * @param {object} targetScreen rivers which we are navigating to * @param {Array<object>} plugins array of plugins in the app * @returns {object} hooksManager object - see Navigator for usage */ export function HooksManager({ rivers, targetScreen, plugins, startUpHooksData = {}, }) { hooksManagerLogger.addContext({ targetScreen }); function logHookEvent(func, message, data) { func({ message, data: __DEV__ ? data : null, }); } const hooksManager = subscriber(); /** * attaches the screen data to the hook object if the hook * is a screen * @param {object} hook object * @returns {object} hook object with river data */ function addScreenDataIfNeeded(hook) { const { screen_id } = hook; const screen = R.pathOr({}, [screen_id], rivers); return R.mergeDeepLeft(hook, screen); } /** * logs to Xray when a hook is missing * @param {object} hook object * @return {function} curried function to be invoked * in ramda's map function */ function logMissingHook(hook) { const hookMessage = (identifier) => `Could not find hook plugin ${identifier} - skipping execution`; return function () { logHookEvent(hooksManagerLogger.warn, hookMessage(hook.identifier), { hook, }); return null; }; } /** * retrieves the hook module in the plugins and adds it to * the hook object if it exists, * @param {object} hook object * @returns {object} hook with plugin module */ function getHookModule(hook) { return R.compose( R.ifElse( R.isNil, logMissingHook(hook), R.compose(R.merge(hook), R.pick(["module", "configuration"])) ), R.find(R.propEq("identifier", hook.identifier)) )(plugins); } /** * returns a Hook object * curried function of the form (x, y) => z => Hook * @param {Array<object>} hooks list of sibling hooks to run in the same process * @param {object} manager hook manager to attach the hook to * @param {object} hook data from the rivers configuration * @returns {Hook} object */ function constructHook(hooks, manager) { return function (hook) { return hook ? new Hook({ hook, hooks, manager }) : null; }; } /** * orders the hooks according to their weight * @param {Array<object>} hooks to sort out * @returns {Array<object>} sorted hooks */ function orderHooksByWeight(hooks) { return R.sortBy(R.compose(Number, R.prop("weight")), hooks); } /** * extracts the hooks form the target river. returns null if there are none * if there are hooks, gathers all required data and constructs a Hook object * @param {string} hookType see constants/HOOK_TYPES * @param {river} river to look for hooks to run * @returns {Array<Hooks>} list of hooks to go through */ function getHooks(hookType, river) { const playerhooks = []; if (riverIsPlayer(river)) { playerhooks.push(...injectPlayerHooks(hookType)); } let hooks = R.concat(playerhooks, R.pathOr([], ["hooks", hookType], river)); hooks = R.sortBy(R.prop("weight"))(hooks); const { startUpHooks, setStartUpHooks } = startUpHooksData; if (startUpHooks && startUpHooks !== "in_process") { hooks.unshift(...startUpHooks); setStartUpHooks("in_process"); } if (!R.length(hooks)) { return null; } return R.compose( R.tap((_hooks) => { const hooksLog = _hooks.map((o) => o?.identifier).join(", "); logHookEvent(hooksManagerLogger.info, `running hooks: ${hooksLog}`, { hooks: _hooks, }); }), R.reject(R.isNil), R.map( R.compose( constructHook(hooks, hooksManager), getHookModule, addScreenDataIfNeeded ) ), orderHooksByWeight )(hooks); } /** * tells wether the current river is related to a player plugin, or the default player * @param {Object} river * @returns {boolean} */ function riverIsPlayer(river) { const { screenType, plugin_type } = river; if (screenType || plugin_type) { return screenType === "playable" || plugin_type === "player"; } const { type } = river; return R.compose( R.propEq("type", "player"), R.defaultTo({}), R.find(R.propEq("identifier", type)) )(plugins); } /** * Injects the hooks for the player * @param {String} hookType type of hook * @returns {Array<Object>} */ function injectPlayerHooks(_hookType) { const playerhooks = R.compose( R.sortBy(R.prop("weight")), R.map((plugin) => ({ identifier: plugin.identifier, weight: R.path(["module", "weight"], plugin) || 1, })), R.filter(R.pathEq(["module", "hasPlayerHook"], true)) )(plugins); return playerhooks; } /** * tells wether the given hook is the last one or not * @param {Hook} hookPlugin * @param {Array<Hook>} restOfHooks * @returns {Boolean} */ function isLastHook(hookPlugin, restOfHooks) { if (restOfHooks?.length === 0) return true; return hookPlugin.lastHook; } /** * creates the callback to invoke in order to proceed to the next hook * covers the scenarios of failure & flow blocking * @param {<Hook>} hookPlugin object for which the callback is built * @param {Array<Hook>} restOfHooks to run * @returns {function} callback function */ function hookCallback(hookPlugin, restOfHooks, initialPayload) { /** * callback invoked after a hook is executed * @param {object} options * @param {boolean} success result of the hook execution * @param {?Error} error returned by the hook if relevant * @param {object} payload returned by the hook, to be passed along to the next hook */ return function ({ success, error, payload = initialPayload, callback, cancelled, }) { if (error) { logHookEvent( hooksManagerLogger.error, `hookCallback: hook returned with an error: ${error.message}`, { payload, hook: hookPlugin, error, } ); return hookPlugin.setStateAndNotify(HOOKS_EVENTS.ERROR, { error, hookPlugin, payload, callback, }); } if (!success) { logHookEvent( hooksManagerLogger.info, `hookCallback: hook was cancelled: ${hookPlugin["identifier"]}`, { payload, hook: hookPlugin, } ); // eslint-disable-next-line // TODO: Temporary hack to pass getLoginProtocol to other plugins to refresh in case token expired, need be deleted later delete payload.getLoginProtocol; hookPlugin.setStateAndNotify(HOOKS_EVENTS.CANCEL, { hookPlugin, payload, }); const isHookInHomescreen = payload?.home; const isHookFlowBlocker = hookPlugin?.module?.isFlowBlocker?.() ?? false; // TODO: We technically need to also support the case where there is more than one hook in the stack, // if needed we could add differentiation explicit user cancellation (when we should go to background) and other types of cancellation if (isHookInHomescreen && isHookFlowBlocker && cancelled) { logHookEvent( hooksManagerLogger.info, `hookCallback: send app to background, cancelled flow blocker hook ${hookPlugin["identifier"]} on home screen`, { payload, hook: hookPlugin, } ); // TODO: Add this logic to platformBack and rename platformBack to platformToBackground for all platforms sendQuickBrickEvent(QUICK_BRICK_EVENTS.MOVE_APP_TO_BACKGROUND, { MOVE_APP_TO_BACKGROUND: true, }); } } else { logHookEvent( hooksManagerLogger.info, `hookCallback: hook successfully finished: ${hookPlugin["identifier"]}`, { payload, hook: hookPlugin, } ); hookPlugin.setStateAndNotify(HOOKS_EVENTS.SUCCESS, { hookPlugin, payload, callback, }); isLastHook(hookPlugin, restOfHooks) ? completeHook(hookPlugin, payload, callback) : handleNextHook(restOfHooks, payload); } }; } /** * flags a hook as succeeded, and pass hook data along * @param {Hook} hookPlugin * @param {object} payload returned by the hook plugin */ function completeHook(hookPlugin, payload, callback) { logHookEvent( hooksManagerLogger.info, `completeHook: hook sequence completed successfully: ${hookPlugin["identifier"]}`, { payload, hook: hookPlugin, } ); // eslint-disable-next-line // TODO: Temporary hack to pass getLoginProtocol to other plugins to refresh in case token expired, need be deleted later delete payload.getLoginProtocol; hookPlugin.setStateAndNotify(HOOKS_EVENTS.COMPLETE, { hookPlugin, payload, callback, }); } /** * presents a screen hook by triggering an event invoking the handler with * the appropriate route & payload * @param {Hook} hookPlugin to present * @param {object} payload to pass to the hook screen * @param {function} callback to invoke when the hook is successful */ hooksManager.presentScreenHook = function (hookPlugin, payload, callback) { const targetScreenRoute = `/hooks/${ hookPlugin.screen_id || hookPlugin.identifier }`; const routeData = { hookPlugin, payload, callback, }; logHookEvent( hooksManagerLogger.info, `presentScreenHook: Presenting screen hook: ${hookPlugin["identifier"]}`, { hook: hookPlugin, payload, } ); hookPlugin.setStateAndNotify(HOOKS_EVENTS.PRESENT_SCREEN_HOOK, { hookPlugin, route: targetScreenRoute, payload: routeData, }); }; /** * executes a headless hook * @param {Hook} hookPlugin to present * @param {object} payload to pass to the hook module * @param {function} callback to invoke when the hook is successful */ hooksManager.executeHook = function (hookPlugin, payload, callback) { logHookEvent( hooksManagerLogger.info, `executeHook: ${hookPlugin["identifier"]}`, { hook: hookPlugin, payload, } ); try { hookPlugin.module.run(payload, callback, hookPlugin.configuration); } catch (error) { logHookEvent( hooksManagerLogger.error, `executeHook: error executing hook: ${hookPlugin["identifier"]} error: ${error.message}`, { hook: hookPlugin, payload, error, } ); hookPlugin.setStateAndNotify(HOOKS_EVENTS.ERROR, { error: error, hookPlugin, payload, }); } }; /** * executes a headless prehook in the background * @param {Hook} hookPlugin to present * @param {object} payload to pass to the hook module * @param {function} callback to invoke when the hook is successful * @param {function} presentUI to call when/if show the ui */ hooksManager.runInBackground = (hookPlugin, payload, callback, presentUI) => { try { logHookEvent( hooksManagerLogger.info, `runInBackground: Executing hook: ${hookPlugin["identifier"]}`, { hook: hookPlugin, payload, } ); hookPlugin.module.runInBackground( payload, callback, hookPlugin, presentUI ); } catch (e) { hookPlugin.setStateAndNotify(HOOKS_EVENTS.ERROR, { error: e, hookPlugin, payload, }); } }; /** * handles the next hook in the list of provided hooks, passing the payload along * @param {Array<Hook>} hooks to process * @param {object} payload to pass to the hook */ function handleNextHook(hooks, payload) { const [hookPlugin, ...restOfHooks] = orderHooksByWeight(hooks); /** * Commenting out part of the code which is trying to run the next hook * in parallel - it's not in use, and it's creating unexpected side * effects * */ hookPlugin.execute(payload, hookCallback(hookPlugin, restOfHooks, payload)); } /** * function exposes on the hooks manager to trigger the handling of the hooks for * the given targetScreen * @param {object} payload to send to the hooks * @returns {object} returns the hooks manager so methods can be chained */ hooksManager.handleHooks = function (payload) { const preloadHooks = getHooks(HOOKS_TYPE.PRELOAD, targetScreen); if (preloadHooks?.length) { const hooks = preloadHooks.map((o) => o?.identifier).join(", "); logHookEvent( hooksManagerLogger.info, `handleHooks: Starting hooks execution, hooks: ${hooks}`, { hooks: preloadHooks, payload, } ); handleNextHook(preloadHooks, payload); } else { hooksManager.invokeHandler(HOOKS_EVENTS.COMPLETE, { hookPlugin: { lastHook: true }, payload, }); } return hooksManager; }; return hooksManager; }