@applicaster/zapp-react-native-utils 16.0.0-alpha.1593888345 → 16.0.0-alpha.1733304298

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 (110) 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/__tests__/StorageActions.test.ts +98 -0
  6. package/actionsExecutor/__tests__/feedDecorator.test.ts +30 -0
  7. package/actionsExecutor/__tests__/screenToggleFlag.test.ts +90 -0
  8. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +139 -5
  9. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
  10. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +160 -0
  11. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  12. package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
  13. package/actionsExecutor/actions/index.ts +2 -0
  14. package/actionsExecutor/actions/navigateToScreen.ts +29 -45
  15. package/actionsExecutor/actions/openBottomSheet.ts +21 -6
  16. package/actionsExecutor/actions/screenToggleFlag.ts +1 -3
  17. package/actionsExecutor/actions/showAlert.ts +51 -0
  18. package/actionsExecutor/actions/showToast.ts +2 -2
  19. package/actionsExecutor/consts.ts +1 -0
  20. package/actionsExecutor/feedDecorator.ts +6 -0
  21. package/analyticsUtils/__tests__/playerAnalyticsSafeguard.test.ts +62 -0
  22. package/analyticsUtils/playerAnalyticsTracker.ts +1 -0
  23. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -22
  24. package/appUtils/contextKeysManager/__tests__/getKey/success/fullSearch.test.ts +1 -25
  25. package/appUtils/contextKeysManager/__tests__/getKey/success/searchInSecure.test.ts +1 -7
  26. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +0 -25
  27. package/appUtils/contextKeysManager/__tests__/removeKey/success.test.ts +0 -6
  28. package/appUtils/contextKeysManager/__tests__/setKey/failure/failedToSaveInStorage.test.ts +0 -40
  29. package/appUtils/contextKeysManager/__tests__/setKey/{success/saveItemAndReference.test.ts → success.test.ts} +2 -42
  30. package/appUtils/contextKeysManager/index.ts +28 -129
  31. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +4 -11
  32. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  33. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +9 -2
  34. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +15 -2
  35. package/appUtils/playerManager/OverlayObserver/utils.ts +2 -5
  36. package/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
  37. package/appUtils/playerManager/__tests__/playerNative.test.ts +76 -0
  38. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  39. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +281 -0
  40. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  41. package/appUtils/playerManager/const.ts +13 -0
  42. package/appUtils/playerManager/index.ts +7 -0
  43. package/appUtils/playerManager/player.ts +25 -0
  44. package/appUtils/playerManager/playerNative.ts +22 -0
  45. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  46. package/appUtils/playerManager/playerToolbox.ts +196 -0
  47. package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
  48. package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
  49. package/arrayUtils/index.ts +26 -43
  50. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  51. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  52. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  53. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  54. package/cellUtils/index.ts +40 -0
  55. package/configurationUtils/__tests__/modalBlocksParser.test.ts +189 -46
  56. package/configurationUtils/modalBlocksParser.ts +57 -101
  57. package/entryActions/__tests__/buildEntryActions.test.ts +97 -18
  58. package/entryActions/aliasPresentation.ts +26 -42
  59. package/entryActions/buildEntryActions.ts +1 -0
  60. package/entryActions/index.ts +0 -2
  61. package/focusManager/FocusManager.ts +26 -0
  62. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  63. package/localizationUtils/index.ts +12 -6
  64. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  65. package/logger/xrayToolbox.ts +67 -0
  66. package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
  67. package/manifestUtils/keys.js +7 -2
  68. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  69. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  70. package/manifestUtils/mobileAction/groups/defaults.js +2 -1
  71. package/modalState/ModalOrchestrator.ts +2 -0
  72. package/modalState/RemoteEditableCollection.ts +9 -1
  73. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +39 -0
  74. package/modalState/__tests__/RemoteEditableCollection.test.ts +90 -1
  75. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  76. package/modalState/index.ts +2 -0
  77. package/modalState/store.ts +9 -2
  78. package/modalState/types.ts +14 -0
  79. package/modalState/useBottomSheetContent.ts +17 -12
  80. package/navigationUtils/xrayNavigator.ts +12 -0
  81. package/package.json +3 -2
  82. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  83. package/pluginUtils/index.ts +29 -0
  84. package/quickBrickAssets/__tests__/index.test.ts +18 -0
  85. package/quickBrickAssets/index.ts +35 -0
  86. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  87. package/reactHooks/actions/index.ts +29 -23
  88. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  89. package/reactHooks/cell-click/index.ts +21 -14
  90. package/reactHooks/connection/index.tsx +8 -5
  91. package/reactHooks/feed/__tests__/feedInvalidation.test.tsx +338 -0
  92. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
  93. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  94. package/reactHooks/feed/useFeedLoader.tsx +28 -1
  95. package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
  96. package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
  97. package/reactHooks/navigation/__tests__/index.test.tsx +128 -1
  98. package/reactHooks/navigation/useRoute.ts +8 -4
  99. package/reactHooks/usePluginConfiguration.ts +84 -46
  100. package/riverComponetsMeasurementProvider/index.tsx +3 -1
  101. package/uiActionsRegistrator/__tests__/subscribe.test.ts +26 -0
  102. package/uiActionsRegistrator/registry.ts +16 -0
  103. package/appUtils/contextKeysManager/__tests__/getKey/success/byReference.test.ts +0 -56
  104. package/appUtils/contextKeysManager/__tests__/getKeyByReference/failure.test.ts +0 -265
  105. package/appUtils/contextKeysManager/__tests__/getKeyByReference/success.test.ts +0 -303
  106. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/failure.test.ts +0 -82
  107. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/success.test.ts +0 -41
  108. package/appUtils/contextKeysManager/__tests__/setKey/success/saveItemButNotReference.test.ts +0 -208
  109. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/failure.test.ts +0 -93
  110. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/success.test.ts +0 -45
@@ -2,6 +2,22 @@ import { openBottomSheetAction } from "../openBottomSheet";
2
2
  import { actionExecutor, ActionResult } from "../../ActionExecutor";
3
3
  import { modalOrchestrator } from "../../../modalState/ModalOrchestrator";
4
4
 
5
+ jest.mock("@applicaster/zapp-pipes-v2-client", () => ({
6
+ PipesClientResponseHelper: jest.fn(() => ({ error: null })),
7
+ RequestBuilder: jest.fn(() => ({
8
+ setEntryContext: jest.fn().mockReturnThis(),
9
+ setScreenContext: jest.fn().mockReturnThis(),
10
+ setUrl: jest.fn().mockReturnThis(),
11
+ buildAxiosRequest: jest.fn().mockResolvedValue(undefined),
12
+ call: jest.fn().mockResolvedValue({
13
+ response: {
14
+ entry: [],
15
+ extensions: { dynamic_collection_options: { operations: "add" } },
16
+ },
17
+ }),
18
+ })),
19
+ }));
20
+
5
21
  jest.mock("../../../modalState/store", () => ({
6
22
  openBottomSheetModal: jest.fn(),
7
23
  }));
@@ -77,6 +93,150 @@ describe("openBottomSheetAction inline items", () => {
77
93
  });
78
94
  });
79
95
 
96
+ it("opens an empty inline collection when dynamic collection options are present", async () => {
97
+ const result = await openBottomSheetAction({
98
+ type: "openBottomSheet",
99
+ options: {
100
+ header: { title: "Queue" },
101
+ content: {
102
+ items: [],
103
+ emptyMessage: "Queued items will be added here",
104
+ dynamicCollection: {
105
+ operations: ["remove"],
106
+ provider: "@{provider/queue}",
107
+ },
108
+ },
109
+ },
110
+ } as any);
111
+
112
+ expect(result).toBe(ActionResult.Success);
113
+
114
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
115
+
116
+ expect(menu.content.items).toEqual([]);
117
+ expect(menu.content.emptyMessage).toBe("Queued items will be added here");
118
+
119
+ expect(menu.content.dynamicCollection).toMatchObject({
120
+ operations: ["remove"],
121
+ provider: "@{provider/queue}",
122
+ });
123
+ });
124
+
125
+ it("accepts empty inline items when dynamic_collection_options live on extensions", async () => {
126
+ const result = await openBottomSheetAction({
127
+ type: "openBottomSheet",
128
+ options: {
129
+ content: {
130
+ items: [],
131
+ extensions: {
132
+ dynamic_collection_options: {
133
+ operations: "remove,reorder",
134
+ provider: "@{provider/offline}",
135
+ },
136
+ },
137
+ },
138
+ },
139
+ } as any);
140
+
141
+ expect(result).toBe(ActionResult.Success);
142
+
143
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
144
+
145
+ expect(menu.content.items).toEqual([]);
146
+
147
+ expect(menu.content.dynamicCollection).toMatchObject({
148
+ operations: ["remove", "reorder"],
149
+ provider: "@{provider/offline}",
150
+ });
151
+ });
152
+
153
+ it("returns an error for an empty items array with no source", async () => {
154
+ const result = await openBottomSheetAction({
155
+ type: "openBottomSheet",
156
+ options: {
157
+ content: { items: [] },
158
+ },
159
+ } as any);
160
+
161
+ expect(result).toBe(ActionResult.Error);
162
+ expect(modalOrchestrator.open).not.toHaveBeenCalled();
163
+ });
164
+
165
+ it("forwards content.emptyMessage onto the menu", async () => {
166
+ const result = await openBottomSheetAction({
167
+ type: "openBottomSheet",
168
+ options: {
169
+ header: { title: "Queue" },
170
+ content: {
171
+ items: [{ id: "t1", title: "Track 1" }],
172
+ emptyMessage: "Queued items will be added here",
173
+ },
174
+ },
175
+ } as any);
176
+
177
+ expect(result).toBe(ActionResult.Success);
178
+
179
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
180
+
181
+ expect(menu.content.emptyMessage).toBe("Queued items will be added here");
182
+ });
183
+
184
+ it("forwards content.operationButtons onto the menu", async () => {
185
+ const result = await openBottomSheetAction({
186
+ type: "openBottomSheet",
187
+ options: {
188
+ header: { title: "Playlists" },
189
+ content: {
190
+ items: [{ id: "p1", title: "Playlist 1" }],
191
+ operationButtons: {
192
+ add: {
193
+ title: "Create New Playlist",
194
+ iconURL: "https://example.com/add.png",
195
+ },
196
+ },
197
+ },
198
+ },
199
+ } as any);
200
+
201
+ expect(result).toBe(ActionResult.Success);
202
+
203
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
204
+
205
+ expect(menu.content.operationButtons).toEqual({
206
+ add: {
207
+ title: "Create New Playlist",
208
+ iconURL: "https://example.com/add.png",
209
+ },
210
+ });
211
+ });
212
+
213
+ it("preserves Add presentation when loading itemsUrl content", async () => {
214
+ const result = await openBottomSheetAction({
215
+ type: "openBottomSheet",
216
+ options: {
217
+ content: {
218
+ itemsUrl: "https://example.com/collections",
219
+ operationButtons: {
220
+ add: {
221
+ title: "Create Collection",
222
+ iconURL: "file:///bundled-add.png",
223
+ },
224
+ },
225
+ },
226
+ },
227
+ });
228
+
229
+ expect(result).toBe(ActionResult.Success);
230
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
231
+
232
+ expect(menu.content).toMatchObject({
233
+ operationButtons: {
234
+ add: { title: "Create Collection", iconURL: "file:///bundled-add.png" },
235
+ },
236
+ dynamicCollection: { operations: ["add"] },
237
+ });
238
+ });
239
+
80
240
  it("pushes a new sheet by default even when another sheet is already open", async () => {
81
241
  (modalOrchestrator as any).stackSize = 1;
82
242
 
@@ -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,9 @@ export async function openBottomSheetAction(
277
289
  dynamicCollection,
278
290
  themePluginId,
279
291
  styleOverrides: styleOverrides ?? content?.styleOverrides,
292
+ emptyMessage: content?.emptyMessage,
293
+ operationButtons: content?.operationButtons,
294
+ context,
280
295
  },
281
296
  };
282
297
 
@@ -27,12 +27,10 @@ export const screenToggleFlagAction: ActionHandler<
27
27
  const screenRoute = context?.screenRoute;
28
28
  const screenStateStore = context?.screenStateStore;
29
29
 
30
- await screenToggleFlag(
30
+ return await screenToggleFlag(
31
31
  screenRoute,
32
32
  screenStateStore,
33
33
  { entry: context?.entry, options: action.options },
34
34
  action
35
35
  );
36
-
37
- return ActionResult.Success;
38
36
  };
@@ -0,0 +1,51 @@
1
+ /// <reference types="@applicaster/applicaster-types" />
2
+ import { showAlertDialog } from "../../alertUtils";
3
+ import { ActionResult } from "../ActionExecutor";
4
+ import { ActionHandler } from "../types";
5
+ import { createLogger } from "../../logger";
6
+
7
+ const { log_info } = createLogger({
8
+ subsystem: "ActionExecutorContext",
9
+ category: "General",
10
+ });
11
+
12
+ /**
13
+ * Options for the showAlert action.
14
+ */
15
+ export interface ShowAlertOptions {
16
+ /** Dialog title (required) */
17
+ title: string;
18
+
19
+ /** Dialog message */
20
+ message?: string;
21
+
22
+ /** Dismiss button text. Defaults to "OK" */
23
+ okButtonText?: string;
24
+ }
25
+
26
+ /**
27
+ * Tells the user something and waits for them to acknowledge it.
28
+ *
29
+ * One button, because there is nothing to decide. Use `confirmDialog` when the
30
+ * user is choosing between going on and backing out; a `confirmDialog` without
31
+ * a cancel label still renders two buttons, the second one blank.
32
+ *
33
+ * Always resolves to Success: dismissing a notice is not a cancellation, so the
34
+ * rest of the chain runs.
35
+ */
36
+ export const showAlertAction: ActionHandler<ShowAlertOptions> = async (
37
+ action
38
+ ): Promise<ActionResult> => {
39
+ log_info("handleAction: showAlert event");
40
+
41
+ const options = action.options as ShowAlertOptions;
42
+
43
+ return new Promise<ActionResult>((resolve) => {
44
+ showAlertDialog({
45
+ title: options?.title,
46
+ message: options?.message,
47
+ okButtonText: options?.okButtonText || "OK",
48
+ completion: () => resolve(ActionResult.Success),
49
+ });
50
+ });
51
+ };
@@ -5,7 +5,7 @@ import { postEvent } from "../../reactHooks/useSubscriberFor";
5
5
 
6
6
  export const CONFIRMATION_TOAST_SOURCE = "confirmation";
7
7
 
8
- export const CONFIRMATION_TOAST_DEFAULT_TIMEOUT = 1000;
8
+ export const CONFIRMATION_TOAST_DEFAULT_TIMEOUT = 5000;
9
9
 
10
10
  /**
11
11
  * Shows a mobile confirmation toast with Theme defaults from
@@ -13,7 +13,7 @@ export const CONFIRMATION_TOAST_DEFAULT_TIMEOUT = 1000;
13
13
  *
14
14
  * This action is confirmation-only by design: it always stamps
15
15
  * `source: "confirmation"` (so Theme confirmation styles apply) and defaults
16
- * `timeout` to 1000 ms. Non-confirmation toasts (e.g. Offline Experience)
16
+ * `timeout` to 5000 ms. Non-confirmation toasts (e.g. Offline Experience)
17
17
  * must post on the shared `"showToast"` event bus directly and omit `source`.
18
18
  * Callers may override individual style fields; Theme supplies the rest.
19
19
  *
@@ -23,6 +23,7 @@ export const ACTION_TYPES = {
23
23
  SWITCH_LAYOUT: "switchLayout",
24
24
  APP_RESTART: "appRestart",
25
25
  CONFIRM_DIALOG: "confirmDialog",
26
+ SHOW_ALERT: "showAlert",
26
27
  SCREEN_SET_VARIABLE: "screenSetVariable",
27
28
  SCREEN_TOGGLE_FLAG: "screenToggleFlag",
28
29
  } as const;
@@ -47,6 +47,11 @@ function makeMultiSelect(feed: ZappFeed, key, decoratedFeed) {
47
47
  ? "sessionStorageToggleFlag"
48
48
  : "localStorageToggleFlag";
49
49
 
50
+ // the toggle actions read their limit from their own options
51
+ const maxItems = Number(
52
+ decoratedFeed.extensions?.preference_editor_options?.max_items
53
+ );
54
+
50
55
  decoratedFeed.entry?.forEach((entry: ZappEntry) => {
51
56
  entry.type.value = "action";
52
57
  entry.id = entry.extensions?.tag || entry.id;
@@ -59,6 +64,7 @@ function makeMultiSelect(feed: ZappFeed, key, decoratedFeed) {
59
64
  type: actionType,
60
65
  options: {
61
66
  key,
67
+ ...(maxItems > 0 && { max_items: maxItems }),
62
68
  },
63
69
  },
64
70
  ],
@@ -0,0 +1,62 @@
1
+ import { PlayerAnalyticsTracker } from "../playerAnalyticsTracker";
2
+ import { PLAYBACK_EVENT } from "../events";
3
+
4
+ const mockPostAnalyticEvent = jest.fn();
5
+
6
+ jest.mock("../manager", () => ({
7
+ postAnalyticEvent: (...args) => mockPostAnalyticEvent(...args),
8
+ }));
9
+
10
+ describe("Player Analytics Safeguards against nativeEvent leakage", () => {
11
+ beforeEach(() => {
12
+ jest.clearAllMocks();
13
+ });
14
+
15
+ const mockEntry = {
16
+ id: "vod_123",
17
+ title: "Test Video",
18
+ type: { value: "video" },
19
+ content: { type: "video/mp4" },
20
+ extensions: { duration: 120 },
21
+ };
22
+
23
+ const mockPlayer = {
24
+ playerPluginId: "quick_brick_player",
25
+ getEntry: () => mockEntry,
26
+ getState: () => ({ isPaused: false, contentPosition: 0.63 }),
27
+ };
28
+
29
+ it("omits nativeEvent from analytics payload if present in eventData", () => {
30
+ const tracker = new PlayerAnalyticsTracker();
31
+ tracker.setPlayer(mockPlayer as any);
32
+
33
+ const payload = tracker.prepareAnalyticPayload(PLAYBACK_EVENT.play, {
34
+ nativeEvent: { currentTime: 0.63, reason: "video_ready" },
35
+ player_type: "quick_brick_player",
36
+ } as any);
37
+
38
+ expect(payload.nativeEvent).toBeUndefined();
39
+ expect(payload.media_id).toBe("vod_123");
40
+ expect(payload.name).toBe("Test Video");
41
+ expect(payload.player_type).toBe("quick_brick_player");
42
+ });
43
+
44
+ it("properly prepares payload when eventData is unwrapped", () => {
45
+ const tracker = new PlayerAnalyticsTracker();
46
+ tracker.setPlayer(mockPlayer as any);
47
+
48
+ const payload = tracker.prepareAnalyticPayload(PLAYBACK_EVENT.play, {
49
+ currentTime: 0.63,
50
+ reason: "video_ready",
51
+ player_type: "quick_brick_player",
52
+ } as any);
53
+
54
+ expect(payload.nativeEvent).toBeUndefined();
55
+ expect(payload.currentTime).toBeUndefined();
56
+ expect(payload.current_position).toBe(0.63);
57
+ expect(payload.reason).toBe("video_ready");
58
+ expect(payload.media_id).toBe("vod_123");
59
+ expect(payload.name).toBe("Test Video");
60
+ expect(payload.player_type).toBe("quick_brick_player");
61
+ });
62
+ });
@@ -214,6 +214,7 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
214
214
  "isLive",
215
215
  "event",
216
216
  "entry",
217
+ "nativeEvent",
217
218
  ],
218
219
  eventData
219
220
  );
@@ -1,4 +1,4 @@
1
- import { ContextKeysManager, NOTHING } from "../..";
1
+ import { ContextKeysManager } from "../..";
2
2
 
3
3
  describe("Context Keys Manager - getKey", () => {
4
4
  it("returns null if key is invalid", async () => {
@@ -34,8 +34,6 @@ describe("Context Keys Manager - getKey", () => {
34
34
  secureStorage: mockedSecureStorage,
35
35
  });
36
36
 
37
- const getKeyByReference = jest.spyOn(contextManager, "getKeyByReference");
38
-
39
37
  // run
40
38
  const result = await contextManager.getKey(invalidKey);
41
39
 
@@ -50,7 +48,6 @@ describe("Context Keys Manager - getKey", () => {
50
48
  data: { key: invalidKey },
51
49
  });
52
50
 
53
- expect(getKeyByReference).not.toHaveBeenCalled();
54
51
  expect(mockedSessionStorage.getItem).not.toHaveBeenCalled();
55
52
  expect(mockedLocalStorage.getItem).not.toHaveBeenCalled();
56
53
  expect(mockedSecureStorage.getItem).not.toHaveBeenCalled();
@@ -90,10 +87,6 @@ describe("Context Keys Manager - getKey", () => {
90
87
  secureStorage: mockedSecureStorage,
91
88
  });
92
89
 
93
- const getKeyByReference = jest
94
- .spyOn(contextManager, "getKeyByReference")
95
- .mockResolvedValueOnce(NOTHING);
96
-
97
90
  // run
98
91
  const result = await contextManager.getKey(key);
99
92
 
@@ -107,8 +100,6 @@ describe("Context Keys Manager - getKey", () => {
107
100
  data: { key },
108
101
  });
109
102
 
110
- expect(getKeyByReference).toHaveBeenCalledWith("key", "namespace");
111
-
112
103
  expect(mockedSessionStorage.getItem).toHaveBeenCalledWith(
113
104
  "key",
114
105
  "namespace"
@@ -152,10 +143,6 @@ describe("Context Keys Manager - getKey", () => {
152
143
  secureStorage: mockedSecureStorage,
153
144
  });
154
145
 
155
- const getKeyByReference = jest
156
- .spyOn(contextManager, "getKeyByReference")
157
- .mockResolvedValueOnce(NOTHING);
158
-
159
146
  // run
160
147
  const result = await contextManager.getKey(key);
161
148
 
@@ -169,8 +156,6 @@ describe("Context Keys Manager - getKey", () => {
169
156
  data: { key },
170
157
  });
171
158
 
172
- expect(getKeyByReference).toHaveBeenCalledWith("key", "namespace");
173
-
174
159
  expect(mockedSessionStorage.getItem).toHaveBeenCalledWith(
175
160
  "key",
176
161
  "namespace"
@@ -214,10 +199,6 @@ describe("Context Keys Manager - getKey", () => {
214
199
  secureStorage: mockedSecureStorage,
215
200
  });
216
201
 
217
- const getKeyByReference = jest
218
- .spyOn(contextManager, "getKeyByReference")
219
- .mockResolvedValueOnce(NOTHING);
220
-
221
202
  // run
222
203
  const result = await contextManager.getKey(key);
223
204
 
@@ -231,8 +212,6 @@ describe("Context Keys Manager - getKey", () => {
231
212
  data: { key },
232
213
  });
233
214
 
234
- expect(getKeyByReference).toHaveBeenCalledWith("key", "namespace");
235
-
236
215
  expect(mockedSessionStorage.getItem).toHaveBeenCalledWith(
237
216
  "key",
238
217
  "namespace"