@applicaster/zapp-react-native-utils 16.0.0-alpha.3112220329 → 16.0.0-alpha.3202041045

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 (82) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/ActionExecutor.ts +10 -1
  3. package/actionsExecutor/ActionExecutorContext.tsx +3 -0
  4. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  5. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +139 -5
  6. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
  7. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +88 -0
  8. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  9. package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
  10. package/actionsExecutor/actions/index.ts +2 -0
  11. package/actionsExecutor/actions/navigateToScreen.ts +29 -45
  12. package/actionsExecutor/actions/openBottomSheet.ts +20 -6
  13. package/actionsExecutor/actions/showAlert.ts +51 -0
  14. package/actionsExecutor/actions/showToast.ts +2 -2
  15. package/actionsExecutor/consts.ts +1 -0
  16. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +4 -11
  17. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  18. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +9 -2
  19. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +15 -2
  20. package/appUtils/playerManager/OverlayObserver/utils.ts +2 -5
  21. package/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
  22. package/appUtils/playerManager/__tests__/playerNative.test.ts +76 -0
  23. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  24. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +281 -0
  25. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  26. package/appUtils/playerManager/const.ts +13 -0
  27. package/appUtils/playerManager/index.ts +7 -0
  28. package/appUtils/playerManager/player.ts +25 -0
  29. package/appUtils/playerManager/playerNative.ts +22 -0
  30. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  31. package/appUtils/playerManager/playerToolbox.ts +196 -0
  32. package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
  33. package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
  34. package/arrayUtils/index.ts +26 -43
  35. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  36. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  37. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  38. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  39. package/cellUtils/index.ts +40 -0
  40. package/configurationUtils/__tests__/modalBlocksParser.test.ts +178 -46
  41. package/configurationUtils/modalBlocksParser.ts +57 -101
  42. package/entryActions/__tests__/buildEntryActions.test.ts +97 -18
  43. package/entryActions/aliasPresentation.ts +26 -42
  44. package/entryActions/buildEntryActions.ts +1 -0
  45. package/entryActions/index.ts +0 -2
  46. package/focusManager/FocusManager.ts +26 -0
  47. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  48. package/localizationUtils/index.ts +12 -6
  49. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  50. package/logger/xrayToolbox.ts +67 -0
  51. package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
  52. package/manifestUtils/keys.js +7 -2
  53. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  54. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  55. package/manifestUtils/mobileAction/groups/defaults.js +2 -1
  56. package/modalState/ModalOrchestrator.ts +1 -0
  57. package/modalState/RemoteEditableCollection.ts +9 -1
  58. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +16 -0
  59. package/modalState/__tests__/RemoteEditableCollection.test.ts +90 -1
  60. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  61. package/modalState/store.ts +9 -2
  62. package/modalState/types.ts +3 -0
  63. package/modalState/useBottomSheetContent.ts +17 -12
  64. package/navigationUtils/xrayNavigator.ts +12 -0
  65. package/package.json +3 -2
  66. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  67. package/pluginUtils/index.ts +29 -0
  68. package/quickBrickAssets/__tests__/index.test.ts +18 -0
  69. package/quickBrickAssets/index.ts +35 -0
  70. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  71. package/reactHooks/actions/index.ts +29 -23
  72. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  73. package/reactHooks/cell-click/index.ts +21 -14
  74. package/reactHooks/connection/index.tsx +8 -5
  75. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  76. package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
  77. package/reactHooks/navigation/__tests__/index.test.tsx +128 -1
  78. package/reactHooks/navigation/useRoute.ts +8 -4
  79. package/reactHooks/usePluginConfiguration.ts +84 -46
  80. package/riverComponetsMeasurementProvider/index.tsx +3 -1
  81. package/uiActionsRegistrator/__tests__/subscribe.test.ts +26 -0
  82. package/uiActionsRegistrator/registry.ts +16 -0
@@ -3,5 +3,7 @@ import { isTV } from "../reactUtils";
3
3
 
4
4
  export const resolveDefaultAssetUri = (source: string | number): string =>
5
5
  isTV()
6
- ? (source as string)
6
+ ? typeof source === "number"
7
+ ? Image.resolveAssetSource(source)?.uri || ""
8
+ : (source as string)
7
9
  : Image.resolveAssetSource(source as number)?.uri || "";
@@ -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;
@@ -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,
@@ -110,6 +111,8 @@ const prepareDefaultActions = (actionExecutor) => {
110
111
  confirmDialogAction
111
112
  );
112
113
 
114
+ actionExecutor.registerAction(ACTION_TYPES.SHOW_ALERT, showAlertAction);
115
+
113
116
  actionExecutor.registerAction(
114
117
  ACTION_TYPES.SEND_CLOUD_EVENT,
115
118
  sendCloudEventAction
@@ -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
+ });
@@ -83,9 +83,7 @@ describe("navigateToScreen navigationOptions", () => {
83
83
  );
84
84
  });
85
85
 
86
- // Entries cannot go through replace(), which only accepts a river, so the
87
- // non-push path falls back to replaceTop().
88
- it("forwards navigationOptions to replaceTop when navigating with an entry", async () => {
86
+ it("forwards navigationOptions to replace when navigating with an entry", async () => {
89
87
  const navigator = createNavigator();
90
88
  const entry = { id: "entry-id" } as unknown as ZappEntry;
91
89
 
@@ -100,12 +98,12 @@ describe("navigateToScreen navigationOptions", () => {
100
98
 
101
99
  await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
102
100
 
103
- expect(navigator.replaceTop).toHaveBeenCalledWith(
101
+ expect(navigator.replace).toHaveBeenCalledWith(
104
102
  { id: "entry-id", type: { value: "parentLock" } },
105
103
  { pinFlow: "change-pin" }
106
104
  );
107
105
 
108
- expect(navigator.replace).not.toHaveBeenCalled();
106
+ expect(navigator.replaceTop).not.toHaveBeenCalled();
109
107
  });
110
108
 
111
109
  // Regression guard: the no-options path must keep working unchanged.
@@ -131,3 +129,139 @@ describe("navigateToScreen navigationOptions", () => {
131
129
  expect(result).toBe(ActionResult.Success);
132
130
  });
133
131
  });
132
+
133
+ describe("navigateToScreen target resolution", () => {
134
+ function run(options, context?) {
135
+ const navigator = createNavigator();
136
+
137
+ const result = createNavigateToScreenAction(
138
+ navigator,
139
+ rivers,
140
+ contentTypes
141
+ )({ type: "navigateToScreen", options }, context);
142
+
143
+ return { navigator, result };
144
+ }
145
+
146
+ it("overrides entry.type with typeMapping when both are provided", async () => {
147
+ const entry = {
148
+ id: "entry-id",
149
+ type: { value: "video" },
150
+ } as unknown as ZappEntry;
151
+
152
+ const { navigator, result } = run({
153
+ typeMapping: "parentLock",
154
+ navigationAction: "push",
155
+ entry,
156
+ });
157
+
158
+ expect(await result).toBe(ActionResult.Success);
159
+
160
+ expect(navigator.push).toHaveBeenCalledWith(
161
+ { id: "entry-id", type: { value: "parentLock" } },
162
+ undefined
163
+ );
164
+
165
+ expect(entry.type).toEqual({ value: "video" });
166
+ });
167
+
168
+ it("navigates with the entry's own type when typeMapping is not provided", async () => {
169
+ const entry = {
170
+ id: "entry-id",
171
+ type: { value: "video" },
172
+ } as unknown as ZappEntry;
173
+
174
+ const { navigator, result } = run({ navigationAction: "push", entry });
175
+
176
+ expect(await result).toBe(ActionResult.Success);
177
+
178
+ expect(navigator.push).toHaveBeenCalledWith(
179
+ { id: "entry-id", type: { value: "video" } },
180
+ undefined
181
+ );
182
+ });
183
+
184
+ it("resolves the context entry when entry is @{entry/} and typeMapping is not provided", async () => {
185
+ const entry = {
186
+ id: "context-entry-id",
187
+ type: { value: "video" },
188
+ } as unknown as ZappEntry;
189
+
190
+ const { navigator, result } = run({ entry: "@{entry/}" }, { entry });
191
+
192
+ expect(await result).toBe(ActionResult.Success);
193
+
194
+ expect(navigator.replace).toHaveBeenCalledWith(
195
+ { id: "context-entry-id", type: { value: "video" } },
196
+ undefined
197
+ );
198
+ });
199
+
200
+ it("returns an error when neither typeMapping nor entry type is provided", async () => {
201
+ const entry = { id: "entry-id" } as unknown as ZappEntry;
202
+
203
+ const { navigator, result } = run({ navigationAction: "push", entry });
204
+
205
+ expect(await result).toBe(ActionResult.Error);
206
+ expect(navigator.push).not.toHaveBeenCalled();
207
+ expect(navigator.replace).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it("returns an error when neither typeMapping nor entry is provided", async () => {
211
+ const { navigator, result } = run({ navigationAction: "push" });
212
+
213
+ expect(await result).toBe(ActionResult.Error);
214
+ expect(navigator.push).not.toHaveBeenCalled();
215
+ });
216
+
217
+ it("returns an error when typeMapping has no screen_id mapping", async () => {
218
+ const callback = jest.fn();
219
+
220
+ const { navigator, result } = run(
221
+ { typeMapping: "unknownType", navigationAction: "push" },
222
+ { callback }
223
+ );
224
+
225
+ expect(await result).toBe(ActionResult.Error);
226
+ expect(navigator.push).not.toHaveBeenCalled();
227
+ expect(callback).not.toHaveBeenCalled();
228
+ });
229
+
230
+ it("returns an error when the mapped screen_id has no river", async () => {
231
+ const navigator = createNavigator();
232
+ const callback = jest.fn();
233
+
234
+ const result = await createNavigateToScreenAction(navigator, rivers, {
235
+ orphan: { screen_id: "missing-screen-id" },
236
+ } as unknown as ZappContentTypes)(
237
+ {
238
+ type: "navigateToScreen",
239
+ options: { typeMapping: "orphan", navigationAction: "push" },
240
+ },
241
+ { callback }
242
+ );
243
+
244
+ expect(result).toBe(ActionResult.Error);
245
+ expect(navigator.push).not.toHaveBeenCalled();
246
+ expect(callback).not.toHaveBeenCalled();
247
+ });
248
+
249
+ it("aborts the action pipeline before navigating to a resolved river", async () => {
250
+ const callback = jest.fn();
251
+
252
+ const { navigator, result } = run(
253
+ { typeMapping: "parentLock" },
254
+ { callback }
255
+ );
256
+
257
+ expect(await result).toBe(ActionResult.Success);
258
+
259
+ expect(callback).toHaveBeenCalledWith({
260
+ success: false,
261
+ error: null,
262
+ abort: true,
263
+ });
264
+
265
+ expect(navigator.replace).toHaveBeenCalledWith(river, undefined);
266
+ });
267
+ });
@@ -60,6 +60,30 @@ describe("openBottomSheetAction custom content", () => {
60
60
  });
61
61
  });
62
62
 
63
+ it("passes themePluginId through to the name sheet", async () => {
64
+ const CustomContent = function CustomContent() {
65
+ return null;
66
+ };
67
+
68
+ await openBottomSheetAction({
69
+ type: "openBottomSheet",
70
+ options: {
71
+ header: { title: "Create New Playlist" },
72
+ themePluginId: "playlist-experience",
73
+ content: {
74
+ component: CustomContent,
75
+ props: { buttonLabel: "Create" },
76
+ },
77
+ },
78
+ } as any);
79
+
80
+ const args = (openBottomSheetModal as jest.Mock).mock.calls[0][0];
81
+
82
+ expect(args.modalBottomSheetContentProps.themePluginId).toBe(
83
+ "playlist-experience"
84
+ );
85
+ });
86
+
63
87
  it("returns Error when neither list data nor content.component is provided", async () => {
64
88
  const result = await openBottomSheetAction({
65
89
  type: "openBottomSheet",
@@ -77,6 +77,94 @@ describe("openBottomSheetAction inline items", () => {
77
77
  });
78
78
  });
79
79
 
80
+ it("opens an empty inline collection when dynamic collection options are present", async () => {
81
+ const result = await openBottomSheetAction({
82
+ type: "openBottomSheet",
83
+ options: {
84
+ header: { title: "Queue" },
85
+ content: {
86
+ items: [],
87
+ emptyMessage: "Queued items will be added here",
88
+ dynamicCollection: {
89
+ operations: ["remove"],
90
+ provider: "@{provider/queue}",
91
+ },
92
+ },
93
+ },
94
+ } as any);
95
+
96
+ expect(result).toBe(ActionResult.Success);
97
+
98
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
99
+
100
+ expect(menu.content.items).toEqual([]);
101
+ expect(menu.content.emptyMessage).toBe("Queued items will be added here");
102
+
103
+ expect(menu.content.dynamicCollection).toMatchObject({
104
+ operations: ["remove"],
105
+ provider: "@{provider/queue}",
106
+ });
107
+ });
108
+
109
+ it("accepts empty inline items when dynamic_collection_options live on extensions", async () => {
110
+ const result = await openBottomSheetAction({
111
+ type: "openBottomSheet",
112
+ options: {
113
+ content: {
114
+ items: [],
115
+ extensions: {
116
+ dynamic_collection_options: {
117
+ operations: "remove,reorder",
118
+ provider: "@{provider/offline}",
119
+ },
120
+ },
121
+ },
122
+ },
123
+ } as any);
124
+
125
+ expect(result).toBe(ActionResult.Success);
126
+
127
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
128
+
129
+ expect(menu.content.items).toEqual([]);
130
+
131
+ expect(menu.content.dynamicCollection).toMatchObject({
132
+ operations: ["remove", "reorder"],
133
+ provider: "@{provider/offline}",
134
+ });
135
+ });
136
+
137
+ it("returns an error for an empty items array with no source", async () => {
138
+ const result = await openBottomSheetAction({
139
+ type: "openBottomSheet",
140
+ options: {
141
+ content: { items: [] },
142
+ },
143
+ } as any);
144
+
145
+ expect(result).toBe(ActionResult.Error);
146
+ expect(modalOrchestrator.open).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it("forwards content.emptyMessage onto the menu", async () => {
150
+ const result = await openBottomSheetAction({
151
+ type: "openBottomSheet",
152
+ options: {
153
+ header: { title: "Queue" },
154
+ content: {
155
+ items: [{ id: "t1", title: "Track 1" }],
156
+ emptyMessage: "Queued items will be added here",
157
+ },
158
+ },
159
+ } as any);
160
+
161
+ expect(result).toBe(ActionResult.Success);
162
+
163
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
164
+
165
+ expect(menu.content.emptyMessage).toBe("Queued items will be added here");
166
+ });
167
+
80
168
  it("pushes a new sheet by default even when another sheet is already open", async () => {
81
169
  (modalOrchestrator as any).stackSize = 1;
82
170
 
@@ -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
  ]);
@@ -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";
@@ -13,7 +13,7 @@ const { log_info, log_error } = createLogger({
13
13
  */
14
14
  export interface NavigateToScreenOptions {
15
15
  /** The screen type to navigate to (from content types mapping) */
16
- typeMapping: string;
16
+ typeMapping?: string;
17
17
 
18
18
  /** Navigation action: push adds to history, replace replaces current screen */
19
19
  navigationAction?: "push" | "replace";
@@ -58,14 +58,6 @@ export function createNavigateToScreenAction(
58
58
  action,
59
59
  context?: ActionExecutionContext
60
60
  ): Promise<ActionResult> {
61
- const screenType = action.options?.typeMapping;
62
-
63
- if (!screenType) {
64
- log_error("navigateToScreen: typeMapping option is missing");
65
-
66
- return ActionResult.Error;
67
- }
68
-
69
61
  const navigationAction = action.options?.navigationAction;
70
62
  const entrySource = action.options?.entry;
71
63
  const navigationOptions = action.options?.navigationOptions;
@@ -76,61 +68,53 @@ export function createNavigateToScreenAction(
76
68
  : entrySource
77
69
  : null;
78
70
 
79
- if (entry) {
80
- if (typeof entry !== "object") {
81
- log_error(
82
- `navigateToScreen: entry option is not an object, entry: ${entry}`
83
- );
71
+ const screenType = action.options?.typeMapping || entry?.type?.value;
84
72
 
85
- return ActionResult.Error;
86
- }
73
+ const navigate =
74
+ navigationAction === "push" ? navigator.push : navigator.replace;
75
+
76
+ let navigatingEntry: ZappEntry | ZappRiver = entry;
87
77
 
78
+ if (entry && screenType) {
88
79
  log_info(
89
80
  `navigateToScreen: navigating to screen type: ${screenType} with entry id: ${entry.id}`
90
81
  );
91
82
 
92
- const overriddenEntry: ZappEntry = {
83
+ navigatingEntry = {
93
84
  ...entry,
94
- type: {
95
- value: screenType,
96
- },
85
+ type: { value: screenType },
97
86
  };
87
+ } else {
88
+ if (!screenType) {
89
+ log_error(
90
+ "navigateToScreen: options.typeMapping or entry?.type?.value is not provided"
91
+ );
98
92
 
99
- if (navigationAction === "push") {
100
- navigator.push(overriddenEntry, navigationOptions);
101
- } else {
102
- // Use replaceTop for entries since replace() only accepts ZappRiver
103
- navigator.replaceTop(overriddenEntry, navigationOptions);
93
+ return ActionResult.Error;
104
94
  }
105
95
 
106
- return ActionResult.Success;
107
- }
108
-
109
- const screenId = contentTypes?.[screenType]?.screen_id || null;
96
+ const screenId = contentTypes?.[screenType]?.screen_id || null;
97
+ const river = rivers?.[screenId];
110
98
 
111
- if (!screenId) {
112
- log_error(
113
- `navigateToScreen: can not resolve screen type mapping: ${screenType}`
114
- );
99
+ if (!screenId) {
100
+ log_error(
101
+ `navigateToScreen: can not resolve screen type mapping: ${screenType}`
102
+ );
115
103
 
116
- return ActionResult.Error;
117
- }
104
+ return ActionResult.Error;
105
+ }
118
106
 
119
- const river = rivers[screenId];
107
+ if (!river) {
108
+ log_error("navigateToScreen: can not resolve river");
120
109
 
121
- if (!river) {
122
- log_error("navigateToScreen: can not resolve river");
110
+ return ActionResult.Error;
111
+ }
123
112
 
124
- return ActionResult.Error;
113
+ navigatingEntry = river;
125
114
  }
126
115
 
127
116
  context?.callback?.({ success: false, error: null, abort: true });
128
-
129
- if (navigationAction === "push") {
130
- navigator.push(river, navigationOptions);
131
- } else {
132
- navigator.replace(river, navigationOptions);
133
- }
117
+ navigate(navigatingEntry, navigationOptions);
134
118
 
135
119
  return ActionResult.Success;
136
120
  };
@@ -208,7 +208,11 @@ export async function openBottomSheetAction(
208
208
  items: [],
209
209
  onPress: () => {},
210
210
  contentComponent: content.component,
211
- contentComponentProps: content.props || {},
211
+ contentComponentProps: {
212
+ ...(content.props || {}),
213
+ actionContext: context,
214
+ },
215
+ themePluginId,
212
216
  },
213
217
  });
214
218
 
@@ -224,7 +228,18 @@ export async function openBottomSheetAction(
224
228
 
225
229
  let dynamicCollection: DynamicCollectionOptions | undefined;
226
230
 
227
- if (Array.isArray(inlineItems) && inlineItems.length > 0) {
231
+ const rawDynamicCollection =
232
+ content?.dynamicCollection ??
233
+ content?.extensions?.dynamic_collection_options;
234
+
235
+ // An empty items array is a valid starting state for a provider-backed or
236
+ // offline collection. A sheet with neither items nor a source still errors.
237
+ const hasDynamicCollection = Boolean(rawDynamicCollection);
238
+
239
+ if (
240
+ Array.isArray(inlineItems) &&
241
+ (inlineItems.length > 0 || hasDynamicCollection)
242
+ ) {
228
243
  log_info("openBottomSheet: using inline items from action options", {
229
244
  count: inlineItems.length,
230
245
  });
@@ -236,10 +251,7 @@ export async function openBottomSheetAction(
236
251
  entryToMenuItem(entry, behavior?.currentSelection ?? [])
237
252
  );
238
253
 
239
- dynamicCollection = parseDynamicCollectionOptions(
240
- content?.dynamicCollection ??
241
- content?.extensions?.dynamic_collection_options
242
- );
254
+ dynamicCollection = parseDynamicCollectionOptions(rawDynamicCollection);
243
255
  } else if (itemsUrl) {
244
256
  log_info(`openBottomSheet: loading items from source: ${itemsUrl}`);
245
257
 
@@ -277,6 +289,8 @@ export async function openBottomSheetAction(
277
289
  dynamicCollection,
278
290
  themePluginId,
279
291
  styleOverrides: styleOverrides ?? content?.styleOverrides,
292
+ emptyMessage: content?.emptyMessage,
293
+ context,
280
294
  },
281
295
  };
282
296