///
import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
import React, { createContext, useEffect, useMemo } from "react";
import {
actionExecutor as _actionExecutor,
ActionResult,
} from "./ActionExecutor";
import {
batchRemoveAllFromNamespaceForStorage,
batchSave,
} from "../zappFrameworkUtils/localStorageHelper";
import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
import * as QuickBrickManager from "@applicaster/zapp-react-native-bridge/QuickBrick";
import { QUICK_BRICK_EVENTS } from "@applicaster/zapp-react-native-bridge/QuickBrick";
import { showConfirmationDialog } from "../alertUtils";
import { createCloudEvent, sendCloudEvent } from "../cloudEventsUtils";
import { createLogger } from "../logger";
import { ACTIVE_LAYOUT_ID_STORAGE_KEY } from "@applicaster/quick-brick-core/App/remoteContextReloader/consts";
import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
import { loadPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
import {
EntryResolver,
resolveObjectValues,
} from "../appUtils/contextKeysManager/contextResolver";
import { useNavigation } from "../reactHooks";
import {
useContentTypes,
usePickFromState,
} from "@applicaster/zapp-react-native-redux/hooks";
import { useSubscriberFor } from "../reactHooks/useSubscriberFor";
import { APP_EVENTS } from "../appUtils/events";
import {
localStorageToggleFlag,
sessionStorageToggleFlag,
} from "./StorageActions";
import { screenSetVariable, screenToggleFlag } from "./ScreenActions";
export const { log_error, log_info, log_debug } = createLogger({
subsystem: "ActionExecutorContext",
category: "General",
});
type ActionExecutorContextType = {
registerAction: (
type: string,
handler: (
action: ActionType,
context?: Record
) => Promise
) => void;
unregisterAction: (type: string) => void;
handleAction: (
action: ActionType,
context?: Record
) => Promise;
handleActions: (
actions: ActionType[],
context: Record
) => Promise;
handleEntryActions: (
entry: ZappEntry,
context?: Record
) => Promise;
};
type Props = {
children: React.ReactNode;
};
function findParentComponent(
childId: string,
parent: ZappRiver | ZappUIComponent
): ZappRiver | ZappUIComponent | null {
for (const child of parent.ui_components) {
if (child.id === childId) return parent;
if (child.ui_components) {
const found = findParentComponent(childId, child);
if (found) return found;
}
}
return null;
}
const prepareDefaultActions = (actionExecutor) => {
actionExecutor.registerAction("localStorageSet", async (action) => {
const namespaces = action.options.content;
await batchSave(namespaces, localStorage);
// TODO: Add support for ownershipKey and ownershipNamespace
return ActionResult.Success;
});
actionExecutor.registerAction("sessionStorageSet", async (action) => {
const namespaces = action.options.content;
await batchSave(namespaces, sessionStorage);
// TODO: Add support for ownershipKey and ownershipNamespace
return ActionResult.Success;
});
actionExecutor.registerAction(
"refreshComponent",
async (_action, context) => {
const dispatch = appStore.getDispatch();
const dataSource =
context?.component?.data?.source ||
findParentComponent(context?.component.id, context?.screenData)?.data
?.source;
log_info(`handleAction: refreshComponent for dataSource:${dataSource}`);
// TODO: In theory we should wait callback to complete, before completing the action, but now it's not needed
// TODO: handle focused item removal
dispatch(
loadPipesData(dataSource, {
silentRefresh: false,
clearCache: true,
riverId: context?.screenData?.id,
})
);
return ActionResult.Success;
}
);
actionExecutor.registerAction("switchLayout", async (action) => {
log_info("handleAction: switchLayout event");
await localStorage.setItem(
ACTIVE_LAYOUT_ID_STORAGE_KEY,
action.options.layoutId
);
QuickBrickManager.sendQuickBrickEvent(QUICK_BRICK_EVENTS.FORCE_APP_RELOAD);
return ActionResult.Success;
});
actionExecutor.registerAction("appRestart", async () => {
log_info(`handleAction: ${QUICK_BRICK_EVENTS.FORCE_APP_RELOAD} event`);
QuickBrickManager.sendQuickBrickEvent(QUICK_BRICK_EVENTS.FORCE_APP_RELOAD);
return ActionResult.Success;
});
actionExecutor.registerAction("confirmDialog", async (action) => {
log_info("handleAction: confirmDialog event");
return new Promise((resolve) => {
showConfirmationDialog({
...action.options,
confirmCompletion: () => resolve(ActionResult.Success),
cancelCompletion: () => resolve(ActionResult.Cancel),
});
});
});
actionExecutor.registerAction("sendCloudEvent", async (action, context) => {
try {
const options = action.options;
const entry = context?.entry || {};
const entryResolver = new EntryResolver(entry);
const screenData = context?.screenStateStore?.getState().data || {};
const screenResolver = new EntryResolver(screenData || {});
const data =
options?.data && options.inflateData
? await resolveObjectValues(options.data, {
entry: entryResolver,
screen: screenResolver,
})
: options?.data || entry;
const cloudEvent = await createCloudEvent({
type: options.type || "com.applicaster.selector.action.v1",
data,
subject: options.subject || entry?.id,
});
log_info("handleAction: sendCloudEvent event", { cloudEvent });
const { error, code } = await sendCloudEvent(cloudEvent, options.url);
if (error) {
log_error("sendCloudEvent: error sending cloud event", { error });
return ActionResult.Error;
}
if (code && code >= 200 && code < 300 && !error) {
log_info("sendCloudEvent: cloud event sent successfully");
return ActionResult.Success;
}
} catch (error) {
log_error("sendCloudEvent: error sending cloud event", {
action,
error,
});
}
return ActionResult.Error;
});
actionExecutor.registerAction(
"sessionStorageToggleFlag",
async (
action: ActionType,
context?: Record
): Promise => {
return await sessionStorageToggleFlag(context, action);
}
);
actionExecutor.registerAction(
"localStorageToggleFlag",
async (
action: ActionType,
context?: Record
): Promise => {
return await localStorageToggleFlag(context, action);
}
);
actionExecutor.registerAction(
"screenSetVariable",
async (
action: ActionType,
context?: Record
): Promise => {
const route = context?.screenRoute;
const screenStateStore = context?.screenStateStore;
await screenSetVariable(
route,
screenStateStore,
{ entry: context?.entry, options: action.options },
action
);
return Promise.resolve(ActionResult.Success);
}
);
actionExecutor.registerAction(
"screenToggleFlag",
async (
action: ActionType,
context?: Record
): Promise => {
const screenRoute = context?.screenRoute;
const screenStateStore = context?.screenStateStore;
await screenToggleFlag(
screenRoute,
screenStateStore,
{ entry: context?.entry, options: action.options },
action
);
return Promise.resolve(ActionResult.Success);
}
);
};
export const ActionExecutorContext =
createContext(null);
export function withActionExecutor(Component) {
prepareDefaultActions(_actionExecutor);
return function ActionExecutorComponent(props: Props) {
const navigator = useNavigation();
const { rivers } = usePickFromState(["rivers"]);
const contentTypes = useContentTypes();
const handlers = useMemo(() => {
return {
unregisterAction: _actionExecutor.unregisterAction,
registerAction: _actionExecutor.registerAction,
handleAction: _actionExecutor.handleAction,
handleActions: _actionExecutor.handleActions,
handleEntryActions: _actionExecutor.handleEntryActions,
};
}, []);
useSubscriberFor(APP_EVENTS.onLogout, () => {
log_debug(
"User profile: onLogout event received, clearing user profile data"
);
const userAccountKey = "user_account";
void batchRemoveAllFromNamespaceForStorage(userAccountKey, localStorage);
void batchRemoveAllFromNamespaceForStorage(
userAccountKey,
sessionStorage
);
});
useEffect(() => {
return _actionExecutor.registerAction(
"navigateToScreen",
async (action: ActionType, context?: Record) => {
const screenType = action.options?.typeMapping;
if (!screenType) {
log_error("navigateToScreen: typeMapping option is missing");
return ActionResult.Error;
}
const navigationAction = action.options?.navigationAction;
const entrySource = action.options?.entry;
const entry = entrySource
? entrySource === "@{entry/}"
? context?.entry
: entrySource
: null;
if (entry) {
if (typeof entry !== "object") {
log_error(
`navigateToScreen: entry option is not an object, entry: ${entry}`
);
return ActionResult.Error;
}
log_info(
`navigateToScreen: navigating to screen type: ${screenType} with entry id: ${entry.id}`
);
const overriddenEntry = {
...entry,
type: {
value: screenType,
},
};
if (navigationAction === "push") {
navigator.push(overriddenEntry);
} else {
navigator.replace(overriddenEntry);
}
return ActionResult.Success;
}
const screenId = contentTypes?.[screenType]?.screen_id || null;
if (!screenId) {
log_error(
`navigateToScreen: can not resolve screen type mapping: ${screenType}`
);
return ActionResult.Error;
}
const river = rivers[screenId];
if (!river) {
log_error("navigateToScreen: can not resolve river");
return ActionResult.Error;
}
context?.callback?.({ success: false, error: null, abort: true });
if (navigationAction === "push") {
navigator.push(river);
} else {
navigator.replace(river);
}
return ActionResult.Success;
}
);
}, [navigator, rivers, contentTypes]);
return (
);
};
}
// TODO: Please use direct import from the ActionExecutor.ts
export const actionExecutor = _actionExecutor;