/// 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;