@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
202 lines (176 loc) • 5.66 kB
text/typescript
import * as R from "ramda";
import { isFunction } from "../../functionUtils";
import { HOOKS_EVENTS, HOOKS_TRANSITION_TYPE } from "./constants";
import { hooksManagerLogger } from "./logger";
import type { Hook as HookInterface, HookIniObj } from "./types";
const BASE_HOOK_PROPERTIES = ["screen_id", "identifier", "weight"];
/**
* returns a numerical value for the given hook event,
* given its position in the declaration (and its place in the
* sequence of events)
* @param {string} hookEventName
* @returns {number} index
*/
const hookState: (
arg0: (typeof HOOKS_EVENTS)[keyof typeof HOOKS_EVENTS]
) => number = R.indexOf(R.__, R.values(HOOKS_EVENTS));
/**
* assigns property to a given context
* @param {object} context to attach properties to
* @return {function} to assign the property value to the context
*/
function assignProp(context) {
return function (value, property) {
context[property] = value;
};
}
/**
* Representation of a hook plugin
*/
export class Hook implements HookInterface {
public manager: HookInterface["manager"];
public lastHook: HookInterface["lastHook"];
public state: HookInterface["state"];
public screen_id: HookInterface["screen_id"];
public identifier: HookInterface["identifier"];
public module: HookInterface["module"];
public general: HookInterface["general"];
public weight: number;
public configuration: HookInterface["configuration"];
constructor({ hook, hooks, manager }: HookIniObj) {
R.forEachObjIndexed(assignProp(this), hook);
this.manager = manager;
this.lastHook = R.equals(
R.pick(BASE_HOOK_PROPERTIES, hook),
R.pick(BASE_HOOK_PROPERTIES, R.last(hooks))
);
this.state = hookState(HOOKS_EVENTS.PENDING);
}
/**
* sets the state of the hook, and invokes the handler for that state
* @param {string} event name
* @param {Array<Any>} args variadic arguments to invoke the handler with
*/
setStateAndNotify(
event: (typeof HOOKS_EVENTS)[keyof typeof HOOKS_EVENTS],
...args
) {
this.state = hookState(event);
this.manager.subscriber.invokeHandler(event, ...args);
}
/**
* returns react component of the the hook modal presenter
* @returns {React.ComponentType<any>}
*/
getModalContainer(): HookInterface["module"]["ModalContainer"] | undefined {
return this.module ? this.module.ModalContainer : undefined;
}
/**
* tells whether the hook should be presented as a screen
* @returns {boolean}
*/
shouldPresentScreenHook(): boolean {
return !!(
(this.screen_id && this.general?.allow_screen_plugin_presentation) ||
(this.module && this.module.presentFullScreen) ||
(!this.module?.presentFullScreen &&
!this.module?.run &&
this.module?.Component) ||
this.isModalHook()
);
}
/**
* tells whether the hook should be presented as a modal screen
* @returns {boolean}
*/
isModalHook(): boolean {
return (
this.screen_id &&
this.general?.transition_type === HOOKS_TRANSITION_TYPE.MODAL
);
}
/**
* tells whether the hook should interupt the whole flow
* if it doesn't return success
* @returns { boolean}
*/
isFlowBlocker(): boolean {
return (
(this.general && this.general.is_flow_blocker) ||
(this.module && this.module.isFlowBlocker && this.module.isFlowBlocker())
);
}
/**
* tells whether the hook should be skipped
* @param {object} payload to pass to the hook
* @returns { boolean}
*/
skipHook(payload: object): boolean {
const _skipHook = R.compose(
R.ifElse(isFunction, R.apply(R.__, [payload]), R.F),
R.path(["module", "skipHook"])
)(this);
if (_skipHook) {
hooksManagerLogger.log({
message: "skipping hook",
data: this,
jsOnly: true,
});
}
return _skipHook;
}
/**
* executes the hook
* @param {object} payload to pass to the hook
* @param {function} next callback to invoke for processing next hook
*/
execute<T extends object>(
payload: T,
next: (arg0: { payload: T; success?: boolean }) => void
): void {
if (this.skipHook(payload)) {
next({ payload, success: true });
return;
}
if (this.state < hookState(HOOKS_EVENTS.START)) {
this.setStateAndNotify(HOOKS_EVENTS.START, this);
if (this?.module?.runInBackground) {
hooksManagerLogger.log({
message: "running hook in background",
data: { hook: this, payload },
jsOnly: true,
});
this.setStateAndNotify(HOOKS_EVENTS.START_BACKGROUND_HOOK, this);
this.manager.runInBackground(this, payload, next, (_payload: any) =>
this.manager.presentScreenHook(this, _payload || payload, next)
);
} else if (this.shouldPresentScreenHook()) {
this.manager.presentScreenHook(this, payload, next);
} else {
this.manager.executeHook(this, payload, next);
}
} else {
// try to go to next hook
// no error, success will be undefined
next({ payload });
}
}
/**
* tells whether the next hook should run in parallel or not
* Will trigger the next in parallel if
* - next hook exists
* - AND currenthook should not present a screen
* - AND next hook has the same weight as the current one
* @returns {boolean}
*/
shouldStartNextHook(nextHook: Hook): boolean {
return (
nextHook &&
!this.shouldPresentScreenHook() &&
R.eqProps("weight", nextHook, this)
);
}
isCancelled(): boolean {
return this.state === hookState(HOOKS_EVENTS.CANCEL);
}
}