@applicaster/zapp-react-native-utils 16.0.0-alpha.1263110541 → 16.0.0-alpha.1334507601

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/actionsExecutor/ActionExecutor.ts +10 -1
  2. package/actionsExecutor/ActionExecutorContext.tsx +13 -1
  3. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  4. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +3 -0
  5. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +113 -1
  6. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  7. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  8. package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
  9. package/actionsExecutor/actions/dismissBottomSheet.ts +2 -1
  10. package/actionsExecutor/actions/index.ts +4 -0
  11. package/actionsExecutor/actions/openBottomSheet.ts +20 -11
  12. package/actionsExecutor/actions/presentScreen.ts +78 -0
  13. package/actionsExecutor/actions/showAlert.ts +51 -0
  14. package/actionsExecutor/actions/showToast.ts +2 -2
  15. package/actionsExecutor/consts.ts +2 -0
  16. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  17. package/configurationUtils/__tests__/modalBlocksParser.test.ts +102 -1
  18. package/configurationUtils/manifestKeyParser.ts +12 -1
  19. package/configurationUtils/modalBlocksParser.ts +26 -4
  20. package/entryActions/__tests__/buildEntryActions.test.ts +97 -18
  21. package/entryActions/aliasPresentation.ts +26 -42
  22. package/entryActions/buildEntryActions.ts +1 -0
  23. package/entryActions/index.ts +0 -2
  24. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  25. package/modalState/ModalOrchestrator.ts +21 -9
  26. package/modalState/RemoteEditableCollection.ts +9 -1
  27. package/modalState/__tests__/RemoteEditableCollection.test.ts +90 -1
  28. package/modalState/__tests__/index.test.ts +30 -1
  29. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  30. package/modalState/index.ts +6 -1
  31. package/modalState/store.ts +9 -2
  32. package/modalState/types.ts +29 -2
  33. package/modalState/useBottomSheetContent.ts +24 -17
  34. package/package.json +2 -2
  35. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  36. package/reactHooks/cell-click/index.ts +21 -14
  37. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  38. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  39. package/reactHooks/navigation/index.ts +2 -0
  40. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  41. package/reactHooks/navigation/useRoute.ts +22 -5
@@ -56,7 +56,6 @@ export class ActionExecutor {
56
56
  context?: ActionExecutionContext
57
57
  ): Promise<ActionResult> => {
58
58
  for (const action of actionTypes) {
59
- // TODO: Handle action result error
60
59
  const result = await this.handleAction(action, context);
61
60
 
62
61
  if (result === ActionResult.Cancel) {
@@ -64,6 +63,16 @@ export class ActionExecutor {
64
63
 
65
64
  return result;
66
65
  }
66
+
67
+ if (result === ActionResult.Error) {
68
+ // Everything after a failed step was written for a state that never
69
+ // happened — the event was refused, the screen never opened — so
70
+ // running it is how a chain ends up telling the user their mail was
71
+ // sent when it was not.
72
+ log_error(`handleActions: action: ${action.type} failed`, { action });
73
+
74
+ return result;
75
+ }
67
76
  }
68
77
 
69
78
  return ActionResult.Success;
@@ -9,7 +9,7 @@ import { ActionExecutionContext, ActionHandler } from "./types";
9
9
  import { batchRemoveAllFromNamespaceForStorage } from "../zappFrameworkUtils/localStorageHelper";
10
10
  import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
11
11
 
12
- import { useNavigation, useRivers } from "../reactHooks";
12
+ import { useNavigation, usePresentScreen, useRivers } from "../reactHooks";
13
13
  import { useContentTypes } from "@applicaster/zapp-react-native-redux/hooks";
14
14
  import { useSubscriberFor } from "../reactHooks/useSubscriberFor";
15
15
  import { APP_EVENTS } from "../appUtils/events";
@@ -18,6 +18,7 @@ import { createLogger } from "../logger";
18
18
  import {
19
19
  appRestartAction,
20
20
  confirmDialogAction,
21
+ showAlertAction,
21
22
  createGoBackAction,
22
23
  createGoHomeAction,
23
24
  createNavigateToScreenAction,
@@ -25,6 +26,7 @@ import {
25
26
  localStorageRemoveAction,
26
27
  localStorageSetAction,
27
28
  localStorageToggleFlagAction,
29
+ createPresentScreenAction,
28
30
  openBottomSheetAction,
29
31
  refreshComponentAction,
30
32
  screenSetVariableAction,
@@ -109,6 +111,8 @@ const prepareDefaultActions = (actionExecutor) => {
109
111
  confirmDialogAction
110
112
  );
111
113
 
114
+ actionExecutor.registerAction(ACTION_TYPES.SHOW_ALERT, showAlertAction);
115
+
112
116
  actionExecutor.registerAction(
113
117
  ACTION_TYPES.SEND_CLOUD_EVENT,
114
118
  sendCloudEventAction
@@ -157,6 +161,7 @@ export function withActionExecutor(Component) {
157
161
  const navigator = useNavigation();
158
162
  const rivers = useRivers();
159
163
  const contentTypes = useContentTypes();
164
+ const presentScreen = usePresentScreen();
160
165
 
161
166
  const handlers = useMemo(() => {
162
167
  return {
@@ -191,6 +196,13 @@ export function withActionExecutor(Component) {
191
196
  );
192
197
  }, [navigator, rivers, contentTypes]);
193
198
 
199
+ useEffect(() => {
200
+ return _actionExecutor.registerAction(
201
+ ACTION_TYPES.PRESENT_SCREEN,
202
+ createPresentScreenAction(presentScreen)
203
+ );
204
+ }, [presentScreen]);
205
+
194
206
  useEffect(() => {
195
207
  return _actionExecutor.registerAction(
196
208
  ACTION_TYPES.GO_BACK,
@@ -0,0 +1,71 @@
1
+ import { ActionExecutor, ActionResult } from "../ActionExecutor";
2
+
3
+ jest.mock("../../logger", () => ({
4
+ createLogger: () => ({
5
+ log_error: jest.fn(),
6
+ log_debug: jest.fn(),
7
+ log_info: jest.fn(),
8
+ }),
9
+ }));
10
+
11
+ describe("handleActions", () => {
12
+ const executor = () => {
13
+ const ran: string[] = [];
14
+ const actionExecutor = new ActionExecutor();
15
+
16
+ const register = (type: string, result?: ActionResult) =>
17
+ actionExecutor.registerAction(type, async () => {
18
+ ran.push(type);
19
+
20
+ return result as ActionResult;
21
+ });
22
+
23
+ register("ok", ActionResult.Success);
24
+ register("fails", ActionResult.Error);
25
+ register("cancels", ActionResult.Cancel);
26
+ register("recover", ActionResult.Success);
27
+ register("after", ActionResult.Success);
28
+
29
+ return { actionExecutor, ran };
30
+ };
31
+
32
+ // Everything after a failed step was written for a state that never
33
+ // happened: the event was refused, the screen never opened.
34
+ it("stops when an action fails", async () => {
35
+ const { actionExecutor, ran } = executor();
36
+
37
+ const result = await actionExecutor.handleActions([
38
+ { type: "fails" },
39
+ { type: "after" },
40
+ ]);
41
+
42
+ expect(ran).toEqual(["fails"]);
43
+ expect(result).toBe(ActionResult.Error);
44
+ });
45
+
46
+ it("still stops on a cancel", async () => {
47
+ const { actionExecutor, ran } = executor();
48
+
49
+ const result = await actionExecutor.handleActions([
50
+ { type: "cancels" },
51
+ { type: "after" },
52
+ ]);
53
+
54
+ expect(ran).toEqual(["cancels"]);
55
+ expect(result).toBe(ActionResult.Cancel);
56
+ });
57
+
58
+ // An action nobody registered never ran, which is a misconfigured feed
59
+ // rather than a step that failed. It is stepped over, as it always was.
60
+ it("steps over an action with no handler", async () => {
61
+ const { actionExecutor, ran } = executor();
62
+
63
+ const result = await actionExecutor.handleActions([
64
+ { type: "nobody-registered-this" },
65
+ { type: "after" },
66
+ ]);
67
+
68
+ expect(ran).toEqual(["after"]);
69
+ expect(result).toBe(ActionResult.Success);
70
+ });
71
+ });
@@ -4,6 +4,9 @@ import { ActionResult } from "../../ActionExecutor";
4
4
 
5
5
  jest.mock("../../../modalState", () => ({
6
6
  dismissModal: jest.fn(),
7
+ modalOrchestrator: {
8
+ close: jest.fn(),
9
+ },
7
10
  }));
8
11
 
9
12
  describe("dismissBottomSheetAction", () => {
@@ -1,5 +1,5 @@
1
1
  import { openBottomSheetAction } from "../openBottomSheet";
2
- import { ActionResult } from "../../ActionExecutor";
2
+ import { actionExecutor, ActionResult } from "../../ActionExecutor";
3
3
  import { modalOrchestrator } from "../../../modalState/ModalOrchestrator";
4
4
 
5
5
  jest.mock("../../../modalState/store", () => ({
@@ -97,4 +97,116 @@ describe("openBottomSheetAction inline items", () => {
97
97
  expect(modalOrchestrator.replace).toHaveBeenCalledTimes(1);
98
98
  expect(modalOrchestrator.open).not.toHaveBeenCalled();
99
99
  });
100
+
101
+ it("executes all actions in tap_actions when onItemPress is called", async () => {
102
+ const multiActionSheet = {
103
+ type: "openBottomSheet",
104
+ options: {
105
+ header: { title: "Target Playlists" },
106
+ content: {
107
+ role: "collection_selector",
108
+ behavior: { selectMode: "none", currentSelection: [] },
109
+ items: [
110
+ {
111
+ id: "playlist-1",
112
+ title: "My Playlist",
113
+ extensions: {
114
+ tap_actions: {
115
+ actions: [
116
+ {
117
+ type: "sendCloudEvent",
118
+ options: { subject: "add_collection_to_collection" },
119
+ },
120
+ {
121
+ type: "showToast",
122
+ options: { message: "Added to playlist" },
123
+ },
124
+ { type: "dismissBottomSheet", options: {} },
125
+ ],
126
+ },
127
+ },
128
+ },
129
+ ],
130
+ },
131
+ },
132
+ } as any;
133
+
134
+ await openBottomSheetAction(multiActionSheet);
135
+
136
+ const [menu, onItemPress] = (modalOrchestrator.open as jest.Mock).mock
137
+ .calls[0];
138
+
139
+ expect(menu.content.items[0].actions).toHaveLength(3);
140
+
141
+ const handleActionsSpy = jest
142
+ .spyOn(actionExecutor, "handleActions")
143
+ .mockResolvedValue(ActionResult.Success);
144
+
145
+ await onItemPress(menu.content.items[0]);
146
+
147
+ expect(handleActionsSpy).toHaveBeenCalledWith(
148
+ [
149
+ {
150
+ type: "sendCloudEvent",
151
+ options: { subject: "add_collection_to_collection" },
152
+ },
153
+ {
154
+ type: "showToast",
155
+ options: { message: "Added to playlist" },
156
+ },
157
+ { type: "dismissBottomSheet", options: {} },
158
+ ],
159
+ expect.objectContaining({
160
+ entry: menu.content.items[0],
161
+ })
162
+ );
163
+
164
+ handleActionsSpy.mockRestore();
165
+ });
166
+
167
+ it("executes preceding actions and excludes presentSubMenu from actionExecutor", async () => {
168
+ const subMenuSheet = {
169
+ type: "openBottomSheet",
170
+ options: {
171
+ header: { title: "Menu" },
172
+ content: {
173
+ items: [
174
+ {
175
+ id: "item-with-submenu",
176
+ title: "More Options",
177
+ extensions: {
178
+ tap_actions: {
179
+ actions: [
180
+ { type: "sendAnalytics", options: { event: "click_more" } },
181
+ {
182
+ type: "presentSubMenu",
183
+ menu: { content: { title: "Sub Options", items: [] } },
184
+ },
185
+ ],
186
+ },
187
+ },
188
+ },
189
+ ],
190
+ },
191
+ },
192
+ } as any;
193
+
194
+ await openBottomSheetAction(subMenuSheet);
195
+
196
+ const [menu, onItemPress] = (modalOrchestrator.open as jest.Mock).mock
197
+ .calls[0];
198
+
199
+ const handleActionsSpy = jest
200
+ .spyOn(actionExecutor, "handleActions")
201
+ .mockResolvedValue(ActionResult.Success);
202
+
203
+ await onItemPress(menu.content.items[0]);
204
+
205
+ expect(handleActionsSpy).toHaveBeenCalledWith(
206
+ [{ type: "sendAnalytics", options: { event: "click_more" } }],
207
+ expect.objectContaining({ entry: menu.content.items[0] })
208
+ );
209
+
210
+ handleActionsSpy.mockRestore();
211
+ });
100
212
  });
@@ -0,0 +1,127 @@
1
+ import { createPresentScreenAction } from "../presentScreen";
2
+ import { ActionResult } from "../../ActionExecutor";
3
+
4
+ const contextEntry = { id: "context-entry" } as any;
5
+
6
+ const succeeded = { success: true, cancelled: false };
7
+
8
+ const cancelled = { success: false, cancelled: true };
9
+
10
+ describe("presentScreenAction", () => {
11
+ let presentScreen: jest.Mock;
12
+ let presentScreenAction: ReturnType<typeof createPresentScreenAction>;
13
+
14
+ beforeEach(() => {
15
+ presentScreen = jest.fn().mockResolvedValue(succeeded);
16
+ presentScreenAction = createPresentScreenAction(presentScreen);
17
+ });
18
+
19
+ it("presents the screen named by typeMapping", async () => {
20
+ await presentScreenAction({
21
+ type: "presentScreen",
22
+ options: { typeMapping: "parent_lock" },
23
+ });
24
+
25
+ expect(presentScreen).toHaveBeenCalledWith({
26
+ typeMapping: "parent_lock",
27
+ entry: undefined,
28
+ });
29
+ });
30
+
31
+ it("fails without presenting anything when typeMapping is missing", async () => {
32
+ const result = await presentScreenAction({
33
+ type: "presentScreen",
34
+ options: {},
35
+ });
36
+
37
+ expect(result).toBe(ActionResult.Error);
38
+ expect(presentScreen).not.toHaveBeenCalled();
39
+ });
40
+
41
+ it("resolves the '@{entry/}' token to the entry from the execution context", async () => {
42
+ await presentScreenAction(
43
+ {
44
+ type: "presentScreen",
45
+ options: { typeMapping: "parent_lock", entry: "@{entry/}" },
46
+ },
47
+ { entry: contextEntry }
48
+ );
49
+
50
+ expect(presentScreen).toHaveBeenCalledWith({
51
+ typeMapping: "parent_lock",
52
+ entry: contextEntry,
53
+ });
54
+ });
55
+
56
+ it("presents an entry supplied inline in the options", async () => {
57
+ const inlineEntry = { id: "inline-entry" } as any;
58
+
59
+ await presentScreenAction({
60
+ type: "presentScreen",
61
+ options: { typeMapping: "parent_lock", entry: inlineEntry },
62
+ });
63
+
64
+ expect(presentScreen).toHaveBeenCalledWith({
65
+ typeMapping: "parent_lock",
66
+ entry: inlineEntry,
67
+ });
68
+ });
69
+
70
+ it("fails without presenting anything when the entry option is not an object", async () => {
71
+ const result = await presentScreenAction({
72
+ type: "presentScreen",
73
+ options: { typeMapping: "parent_lock", entry: "not-an-entry" as any },
74
+ });
75
+
76
+ expect(result).toBe(ActionResult.Error);
77
+ expect(presentScreen).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it("succeeds when the presented screen reports success", async () => {
81
+ presentScreen.mockResolvedValue(succeeded);
82
+
83
+ const result = await presentScreenAction({
84
+ type: "presentScreen",
85
+ options: { typeMapping: "parent_lock" },
86
+ });
87
+
88
+ expect(result).toBe(ActionResult.Success);
89
+ });
90
+
91
+ it("cancels the action chain when the presented screen is cancelled", async () => {
92
+ presentScreen.mockResolvedValue(cancelled);
93
+
94
+ const result = await presentScreenAction({
95
+ type: "presentScreen",
96
+ options: { typeMapping: "parent_lock" },
97
+ });
98
+
99
+ expect(result).toBe(ActionResult.Cancel);
100
+ });
101
+
102
+ it("cancels the action chain when the presented screen reports failure", async () => {
103
+ presentScreen.mockResolvedValue({ success: false, cancelled: false });
104
+
105
+ const result = await presentScreenAction({
106
+ type: "presentScreen",
107
+ options: { typeMapping: "parent_lock" },
108
+ });
109
+
110
+ expect(result).toBe(ActionResult.Cancel);
111
+ });
112
+
113
+ it("errors when the screen could not be presented at all", async () => {
114
+ presentScreen.mockResolvedValue({
115
+ success: false,
116
+ cancelled: false,
117
+ error: "can not resolve screen type mapping: parent_lock",
118
+ });
119
+
120
+ const result = await presentScreenAction({
121
+ type: "presentScreen",
122
+ options: { typeMapping: "parent_lock" },
123
+ });
124
+
125
+ expect(result).toBe(ActionResult.Error);
126
+ });
127
+ });
@@ -0,0 +1,49 @@
1
+ import { showAlertAction } from "../showAlert";
2
+ import { ActionResult } from "../../ActionExecutor";
3
+ import { showAlertDialog } from "../../../alertUtils";
4
+
5
+ jest.mock("../../../alertUtils", () => ({
6
+ showAlertDialog: jest.fn(),
7
+ }));
8
+
9
+ const dialog = showAlertDialog as jest.Mock;
10
+
11
+ const dismiss = () => dialog.mock.calls[0][0].completion();
12
+
13
+ describe("showAlert", () => {
14
+ beforeEach(() => dialog.mockReset());
15
+
16
+ it("shows the notice and resolves once it is dismissed", async () => {
17
+ const result = showAlertAction({
18
+ type: "showAlert",
19
+ options: { title: "Locked", message: "Ask the account owner." },
20
+ });
21
+
22
+ expect(dialog).toHaveBeenCalledWith(
23
+ expect.objectContaining({
24
+ title: "Locked",
25
+ message: "Ask the account owner.",
26
+ okButtonText: "OK",
27
+ })
28
+ );
29
+
30
+ dismiss();
31
+
32
+ // Dismissing a notice is not a cancellation: the chain carries on.
33
+ await expect(result).resolves.toBe(ActionResult.Success);
34
+ });
35
+
36
+ it("takes the dismiss label from the action", async () => {
37
+ const result = showAlertAction({
38
+ type: "showAlert",
39
+ options: { title: "Locked", okButtonText: "Got it" },
40
+ });
41
+
42
+ expect(dialog).toHaveBeenCalledWith(
43
+ expect.objectContaining({ okButtonText: "Got it" })
44
+ );
45
+
46
+ dismiss();
47
+ await result;
48
+ });
49
+ });
@@ -31,7 +31,7 @@ describe("showToastAction", () => {
31
31
  message: "Added to queue",
32
32
  extraMessage: "Extra info",
33
33
  style: undefined,
34
- timeout: 1000,
34
+ timeout: 5000,
35
35
  source: "confirmation",
36
36
  },
37
37
  ]);
@@ -78,7 +78,7 @@ describe("showToastAction", () => {
78
78
  message: undefined,
79
79
  extraMessage: undefined,
80
80
  style: undefined,
81
- timeout: 1000,
81
+ timeout: 5000,
82
82
  source: "confirmation",
83
83
  },
84
84
  ]);
@@ -1,5 +1,5 @@
1
1
  /// <reference types="@applicaster/applicaster-types" />
2
- import { dismissModal } from "../../modalState";
2
+ import { dismissModal, modalOrchestrator } from "../../modalState";
3
3
  import { ActionResult } from "../ActionExecutor";
4
4
  import { ActionHandler } from "../types";
5
5
  import { createLogger } from "../../logger";
@@ -16,6 +16,7 @@ export const dismissBottomSheetAction: ActionHandler<
16
16
  any
17
17
  > = async (): Promise<ActionResult> => {
18
18
  log_info("handleAction: dismissBottomSheet");
19
+ modalOrchestrator.close();
19
20
  dismissModal();
20
21
 
21
22
  return ActionResult.Success;
@@ -15,6 +15,8 @@ export { appRestartAction } from "./appRestart";
15
15
 
16
16
  export { confirmDialogAction } from "./confirmDialog";
17
17
 
18
+ export { showAlertAction } from "./showAlert";
19
+
18
20
  export { sendCloudEventAction } from "./sendCloudEvent";
19
21
 
20
22
  export { sessionStorageToggleFlagAction } from "./sessionStorageToggleFlag";
@@ -27,6 +29,8 @@ export { screenToggleFlagAction } from "./screenToggleFlag";
27
29
 
28
30
  export { createNavigateToScreenAction } from "./navigateToScreen";
29
31
 
32
+ export { createPresentScreenAction } from "./presentScreen";
33
+
30
34
  export { createGoBackAction } from "./goBack";
31
35
 
32
36
  export { createGoHomeAction } from "./goHome";
@@ -8,6 +8,7 @@ import { modalOrchestrator } from "../../modalState/ModalOrchestrator";
8
8
  import { openBottomSheetModal } from "../../modalState/store";
9
9
  import {
10
10
  DynamicCollectionOptions,
11
+ getMenuItemActions,
11
12
  isPresentSubMenuAction,
12
13
  Menu,
13
14
  MenuItem,
@@ -91,6 +92,7 @@ export function entryToMenuItem(
91
92
  icon: firstImage?.src,
92
93
  isSelected: currentSelection.includes(itemId),
93
94
  action: primaryAction,
95
+ actions: tapActions,
94
96
  entryActions: entry.extensions?.entry_action || [],
95
97
  };
96
98
  }
@@ -206,7 +208,10 @@ export async function openBottomSheetAction(
206
208
  items: [],
207
209
  onPress: () => {},
208
210
  contentComponent: content.component,
209
- contentComponentProps: content.props || {},
211
+ contentComponentProps: {
212
+ ...(content.props || {}),
213
+ actionContext: context,
214
+ },
210
215
  },
211
216
  });
212
217
 
@@ -275,6 +280,7 @@ export async function openBottomSheetAction(
275
280
  dynamicCollection,
276
281
  themePluginId,
277
282
  styleOverrides: styleOverrides ?? content?.styleOverrides,
283
+ context,
278
284
  },
279
285
  };
280
286
 
@@ -288,18 +294,21 @@ export async function openBottomSheetAction(
288
294
  * reference it.
289
295
  */
290
296
  const onItemPress = async (item: MenuItem): Promise<void> => {
291
- if (!item.action || isPresentSubMenuAction(item.action)) {
292
- // Submenu navigation is handled internally by the orchestrator
293
- return;
294
- }
297
+ const actions = getMenuItemActions(item);
295
298
 
296
- await actionExecutor.handleAction(
297
- item.action as ActionType,
298
- {
299
- ...context,
300
- entry: item as any,
301
- } as any
299
+ const executableActions = actions.filter(
300
+ (action) => !isPresentSubMenuAction(action)
302
301
  );
302
+
303
+ if (executableActions.length > 0) {
304
+ await actionExecutor.handleActions(
305
+ executableActions as ActionType[],
306
+ {
307
+ ...context,
308
+ entry: item as any,
309
+ } as any
310
+ );
311
+ }
303
312
  };
304
313
 
305
314
  if (action.options?.replace) {
@@ -0,0 +1,78 @@
1
+ /// <reference types="@applicaster/applicaster-types" />
2
+ import { ActionResult } from "../ActionExecutor";
3
+ import { ActionHandler, ActionExecutionContext } from "../types";
4
+ import type { PresentScreenFn } from "../../reactHooks/navigation/usePresentScreen";
5
+ import { createLogger } from "../../logger";
6
+
7
+ const { log_error } = createLogger({
8
+ subsystem: "ActionExecutorContext",
9
+ category: "General",
10
+ });
11
+
12
+ /**
13
+ * Options for the presentScreen action.
14
+ */
15
+ export interface PresentScreenOptions {
16
+ /** The screen type to present (from content types mapping) */
17
+ typeMapping: string;
18
+
19
+ /**
20
+ * Entry to present the screen with. Can be:
21
+ * - "@{entry/}" to use the context entry
22
+ * - A ZappEntry object
23
+ * - undefined to present the screen without an entry
24
+ */
25
+ entry?: "@{entry/}" | ZappEntry;
26
+ }
27
+
28
+ /**
29
+ * Creates a present-screen action handler with the provided dependencies.
30
+ *
31
+ * Unlike navigateToScreen, this action does not return until the presented
32
+ * screen reports a result, so a screen that gates the flow (a parental lock,
33
+ * for example) can stop the remaining actions by being cancelled.
34
+ *
35
+ * @param presentScreen - Presents a screen as a modal and resolves with its result
36
+ * @returns An action handler that presents a screen and awaits its outcome
37
+ */
38
+ export function createPresentScreenAction(
39
+ presentScreen: PresentScreenFn
40
+ ): ActionHandler<PresentScreenOptions> {
41
+ return async function presentScreenAction(
42
+ action,
43
+ context?: ActionExecutionContext
44
+ ): Promise<ActionResult> {
45
+ const typeMapping = action.options?.typeMapping;
46
+
47
+ if (!typeMapping) {
48
+ log_error("presentScreen: typeMapping option is missing");
49
+
50
+ return ActionResult.Error;
51
+ }
52
+
53
+ const entrySource = action.options?.entry;
54
+
55
+ const entry = entrySource
56
+ ? entrySource === "@{entry/}"
57
+ ? context?.entry
58
+ : entrySource
59
+ : undefined;
60
+
61
+ if (entry && typeof entry !== "object") {
62
+ log_error(
63
+ `presentScreen: entry option is not an object, entry: ${entry}`
64
+ );
65
+
66
+ return ActionResult.Error;
67
+ }
68
+
69
+ const result = await presentScreen({ typeMapping, entry });
70
+
71
+ if (result.error) {
72
+ return ActionResult.Error;
73
+ }
74
+
75
+ // Anything short of success stops the remaining actions in the chain.
76
+ return result.success ? ActionResult.Success : ActionResult.Cancel;
77
+ };
78
+ }