@applicaster/zapp-react-native-utils 15.0.0-alpha.4200413542 → 15.0.0-alpha.4225160176

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 (33) hide show
  1. package/README.md +0 -6
  2. package/actionsExecutor/ActionExecutorContext.tsx +44 -1
  3. package/appUtils/HooksManager/index.ts +35 -0
  4. package/appUtils/focusManager/treeDataStructure/Tree/__tests__/Tree.test.js +46 -0
  5. package/appUtils/focusManager/treeDataStructure/Tree/index.js +18 -18
  6. package/appUtils/focusManagerAux/utils/index.ts +2 -4
  7. package/appUtils/focusManagerAux/utils/utils.ios.ts +6 -3
  8. package/appUtils/localizationsHelper.ts +4 -0
  9. package/appUtils/playerManager/playerNative.ts +2 -1
  10. package/componentsUtils/index.ts +8 -0
  11. package/manifestUtils/defaultManifestConfigurations/player.js +6 -16
  12. package/numberUtils/__tests__/toNumber.test.ts +27 -12
  13. package/numberUtils/__tests__/toPositiveNumber.test.ts +32 -4
  14. package/numberUtils/index.ts +5 -1
  15. package/package.json +3 -3
  16. package/reactHooks/feed/index.ts +0 -2
  17. package/reactHooks/feed/useInflatedUrl.ts +1 -1
  18. package/reactHooks/resolvers/useComponentResolver.ts +11 -1
  19. package/reactHooks/state/__tests__/useComponentScreenState.test.ts +246 -0
  20. package/reactHooks/state/index.ts +2 -0
  21. package/reactHooks/state/useComponentScreenState.ts +45 -0
  22. package/refreshUtils/RefreshCoordinator/__tests__/refreshCoordinator.test.ts +161 -0
  23. package/refreshUtils/RefreshCoordinator/index.ts +216 -0
  24. package/refreshUtils/RefreshCoordinator/utils/__tests__/getDataRefreshConfig.test.ts +104 -0
  25. package/refreshUtils/RefreshCoordinator/utils/index.ts +29 -0
  26. package/screenPickerUtils/index.ts +5 -0
  27. package/screenUtils/index.ts +3 -0
  28. package/utils/__tests__/clone.test.ts +158 -0
  29. package/utils/__tests__/path.test.ts +7 -0
  30. package/utils/clone.ts +7 -0
  31. package/utils/index.ts +2 -1
  32. package/reactHooks/feed/__tests__/useFeedRefresh.test.tsx +0 -75
  33. package/reactHooks/feed/useFeedRefresh.tsx +0 -65
package/README.md CHANGED
@@ -245,12 +245,6 @@ const connectionType = useConnectionInfo(true);
245
245
 
246
246
  `@applicaster/zapp-react-native/reactHooks`
247
247
 
248
- - `useFeedRefresh: ({ reloadData: function, component: { id: boolean | string, rules: {enable_data_refreshing: boolean, refreshing_interval: number} } }) => void` - Hook will call `reloadData` function, in the specified intervals if `enable_data_refreshing` is set to true;
249
-
250
- ```javascript
251
- useFeedRefresh({ reloadData, component });
252
- ```
253
-
254
248
  - `useFeedLoader: ({ feedUrl: string, pipesOptions?: { clearCache?: boolean, loadLocalFavourites?: boolean, silentRefresh?: boolean} }) => ({data: ?ApplicasterFeed, loading: boolean, url: string, error: Error,reloadData: (silentRefresh?: boolean) => void, loadNext: () => void})` - Hook will load data to the redux store and return a feed for the provided DSP URL. If the data for the provided url was already loaded, it will return that value
255
249
 
256
250
  ```javascript
@@ -315,6 +315,44 @@ export function withActionExecutor(Component) {
315
315
  return ActionResult.Error;
316
316
  }
317
317
 
318
+ const navigationAction = action.options?.navigationAction;
319
+ const entrySource = action.options?.entry;
320
+
321
+ const entry = entrySource
322
+ ? entrySource === "@{entry/}"
323
+ ? context?.entry
324
+ : entrySource
325
+ : null;
326
+
327
+ if (entry) {
328
+ if (typeof entry !== "object") {
329
+ log_error(
330
+ `navigateToScreen: entry option is not an object, entry: ${entry}`
331
+ );
332
+
333
+ return ActionResult.Error;
334
+ }
335
+
336
+ log_info(
337
+ `navigateToScreen: navigating to screen type: ${screenType} with entry id: ${entry.id}`
338
+ );
339
+
340
+ const overriddenEntry = {
341
+ ...entry,
342
+ type: {
343
+ value: screenType,
344
+ },
345
+ };
346
+
347
+ if (navigationAction === "push") {
348
+ navigator.push(overriddenEntry);
349
+ } else {
350
+ navigator.replace(overriddenEntry);
351
+ }
352
+
353
+ return ActionResult.Success;
354
+ }
355
+
318
356
  const screenId = contentTypes?.[screenType]?.screen_id || null;
319
357
 
320
358
  if (!screenId) {
@@ -334,7 +372,12 @@ export function withActionExecutor(Component) {
334
372
  }
335
373
 
336
374
  context?.callback?.({ success: false, error: null, abort: true });
337
- navigator.replace(river);
375
+
376
+ if (navigationAction === "push") {
377
+ navigator.push(river);
378
+ } else {
379
+ navigator.replace(river);
380
+ }
338
381
 
339
382
  return ActionResult.Success;
340
383
  }
@@ -11,6 +11,10 @@ import { HOOKS_EVENTS, HOOKS_TYPE } from "./constants";
11
11
 
12
12
  import { hooksManagerLogger } from "./logger";
13
13
  import { HookManager, HookManagerArgs } from "./types";
14
+ import {
15
+ actionExecutor,
16
+ ActionResult,
17
+ } from "../../actionsExecutor/ActionExecutor";
14
18
 
15
19
  /**
16
20
  * orders the hooks according to their weight
@@ -280,6 +284,8 @@ export function HooksManager({
280
284
  {}
281
285
  );
282
286
 
287
+ actionExecutor.unregisterAction("finishHook");
288
+
283
289
  return;
284
290
  }
285
291
 
@@ -294,6 +300,8 @@ export function HooksManager({
294
300
  }
295
301
  );
296
302
 
303
+ actionExecutor.unregisterAction("finishHook");
304
+
297
305
  return hookPlugin.setStateAndNotify(HOOKS_EVENTS.ERROR, {
298
306
  error,
299
307
  hookPlugin,
@@ -316,6 +324,8 @@ export function HooksManager({
316
324
  // TODO: Temporary hack to pass getLoginProtocol to other plugins to refresh in case token expired, need be deleted later
317
325
  delete payload.getLoginProtocol;
318
326
 
327
+ actionExecutor.unregisterAction("finishHook");
328
+
319
329
  hookPlugin.setStateAndNotify(HOOKS_EVENTS.CANCEL, {
320
330
  hookPlugin,
321
331
  payload,
@@ -381,6 +391,27 @@ export function HooksManager({
381
391
  };
382
392
  }
383
393
 
394
+ function registerFinishHookAction(payload, callback) {
395
+ // Ensure no stale finishHook remains (e.g. presentUI re-entry from runInBackground)
396
+ actionExecutor.unregisterAction("finishHook");
397
+
398
+ actionExecutor.registerAction("finishHook", async (action: ActionType) => {
399
+ const { success, errorMessage, abort } = action.options;
400
+
401
+ actionExecutor.unregisterAction("finishHook");
402
+
403
+ if (errorMessage) {
404
+ callback({ success, error: new Error(errorMessage), payload, abort });
405
+ } else {
406
+ callback({ success, error: null, payload, abort });
407
+ }
408
+
409
+ hooksManagerLogger.info("finishHook action executed, finishing flow");
410
+
411
+ return ActionResult.Success;
412
+ });
413
+ }
414
+
384
415
  /**
385
416
  * presents a screen hook by triggering an event invoking the handler with
386
417
  * the appropriate route & payload
@@ -408,6 +439,8 @@ export function HooksManager({
408
439
  }
409
440
  );
410
441
 
442
+ registerFinishHookAction(payload, callback);
443
+
411
444
  hookPlugin.setStateAndNotify(HOOKS_EVENTS.PRESENT_SCREEN_HOOK, {
412
445
  hookPlugin,
413
446
  route: targetScreenRoute,
@@ -467,6 +500,8 @@ export function HooksManager({
467
500
  }
468
501
  );
469
502
 
503
+ registerFinishHookAction(payload, callback);
504
+
470
505
  hookPlugin.module.runInBackground(
471
506
  payload,
472
507
  callback,
@@ -373,3 +373,49 @@ describe("addNode", () => {
373
373
  checkParents(tree.root);
374
374
  });
375
375
  });
376
+
377
+ describe("findInTree", () => {
378
+ function createNode(id, children) {
379
+ return {
380
+ id,
381
+ children,
382
+ };
383
+ }
384
+
385
+ it("returns a direct child match from root children", () => {
386
+ const tree = new Tree(treeLoaded);
387
+ const direct = createNode("direct-node");
388
+
389
+ tree.root.children = [direct];
390
+
391
+ expect(tree.findInTree("direct-node")).toEqual(direct);
392
+ });
393
+
394
+ it("returns a nested descendant match", () => {
395
+ const tree = new Tree(treeLoaded);
396
+ const nested = createNode("nested-node");
397
+ const intermediate = createNode("intermediate-node", [nested]);
398
+ const rootNode = createNode("root-node", [intermediate]);
399
+
400
+ tree.root.children = [rootNode];
401
+
402
+ expect(tree.findInTree("nested-node")).toEqual(nested);
403
+ });
404
+
405
+ it("returns null when node id does not exist", () => {
406
+ const tree = new Tree(treeLoaded);
407
+ const leaf = createNode("leaf-node");
408
+ const rootNode = createNode("root-node", [leaf]);
409
+
410
+ tree.root.children = [rootNode];
411
+
412
+ expect(tree.findInTree("missing-node")).toEqual(null);
413
+ });
414
+
415
+ it("returns null when tree has no children", () => {
416
+ const tree = new Tree(treeLoaded);
417
+ tree.root.children = null;
418
+
419
+ expect(tree.findInTree("any-node")).toEqual(null);
420
+ });
421
+ });
@@ -205,31 +205,31 @@ export class Tree {
205
205
  * @returns founded node or null
206
206
  */
207
207
  findInTree(id) {
208
- const retVal = null;
209
-
210
- return this.findInArray(id, this.root.children, retVal);
208
+ return this.findInArray(id, this.root.children);
211
209
  }
212
210
 
213
- findInArray(id, children, retVal) {
214
- if (!retVal && children) {
215
- retVal = children.find((obj) => obj.id === id);
211
+ findInArray(id, children) {
212
+ if (!children) {
213
+ return null;
214
+ }
216
215
 
217
- if (!retVal) {
218
- children.forEach((child) => {
219
- if (child.children) {
220
- retVal = this.findInArray(id, child.children, retVal);
216
+ const directMatch = children.find((obj) => obj.id === id);
221
217
 
222
- if (retVal) {
223
- return retVal;
224
- }
225
- }
226
- });
227
- } else {
228
- return retVal;
218
+ if (directMatch) {
219
+ return directMatch;
220
+ }
221
+
222
+ for (const child of children) {
223
+ if (child.children) {
224
+ const nestedMatch = this.findInArray(id, child.children);
225
+
226
+ if (nestedMatch) {
227
+ return nestedMatch;
228
+ }
229
229
  }
230
230
  }
231
231
 
232
- return retVal;
232
+ return null;
233
233
  }
234
234
 
235
235
  /**
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  import {
15
15
  getFocusableId,
16
- SCREEN_PICKER_CONTAINER,
16
+ isTabsScreenContentContainerId,
17
17
  } from "@applicaster/zapp-react-native-utils/screenPickerUtils";
18
18
 
19
19
  // run check each 300 ms
@@ -24,8 +24,6 @@ const isTopMenu = (node) => startsWith(QUICK_BRICK_NAVBAR, node?.id);
24
24
  const isContent = (node) => startsWith(QUICK_BRICK_CONTENT, node?.id);
25
25
  const isRoot = (node) => node?.id === "root";
26
26
 
27
- const isScrenPicker = (node) => startsWith(SCREEN_PICKER_CONTAINER, node?.id);
28
-
29
27
  type Props = {
30
28
  maxTimeout: number;
31
29
  conditionFn: () => boolean;
@@ -136,7 +134,7 @@ export const isTabsScreenOnContentFocused = (node) => {
136
134
  return false;
137
135
  }
138
136
 
139
- if (isScrenPicker(node)) {
137
+ if (isTabsScreenContentContainerId(node?.id)) {
140
138
  return true;
141
139
  }
142
140
 
@@ -106,9 +106,8 @@ const focusableNativeViewRegistration = ({ focusableView, focusableGroup }) => {
106
106
  );
107
107
  };
108
108
 
109
- export const firstFocusableViewRegistrationFactory = () =>
109
+ export const firstFocusableViewInContentRegistrationFactory = () =>
110
110
  focusableViewRegistrationSubject$.pipe(
111
- take(1), // we care about only first FocusableView registration
112
111
  switchMap((focusableView) =>
113
112
  // start waiting registration of its parent FocusableGroup
114
113
  focusableGroupRegistrationSubject$.pipe(
@@ -126,7 +125,11 @@ export const firstFocusableViewRegistrationFactory = () =>
126
125
  focusableView,
127
126
  focusableGroup,
128
127
  })
129
- )
128
+ ),
129
+ filter(({ focusableView }) =>
130
+ isPartOfContent(focusManager.focusableTree, focusableView.id)
131
+ ),
132
+ take(1) // we care about only first FocusableView registration
130
133
  );
131
134
 
132
135
  // registration on RN level(into RN focusManager)
@@ -85,6 +85,10 @@ export const toDayJSLocaleMap = (code) => {
85
85
  tt: null,
86
86
  "es-LA": "es",
87
87
  "en-UK": "en-gb",
88
+ "en-ZA": null,
89
+ "en-NG": null,
90
+ "en-ZW": null,
91
+ "en-ZM": null,
88
92
  };
89
93
 
90
94
  if (map[code] === null) {
@@ -229,8 +229,9 @@ export class PlayerNative extends Player {
229
229
  };
230
230
 
231
231
  closeNativePlayer = () => {
232
- // TODO: Delete does not work
232
+ // TODO: Delete, does not work (component is null)
233
233
  this.currentPlayerComponent()?.closeNativePlayer?.();
234
+ this.getPlayerModule()?.stopBackgroundPlayback?.();
234
235
  };
235
236
 
236
237
  togglePlayPause = () => {
@@ -1,3 +1,5 @@
1
+ import { last } from "@applicaster/zapp-react-native-utils/utils";
2
+
1
3
  const GROUP = "group-qb";
2
4
  const GROUP_INFO = "group-info-qb";
3
5
  const GROUP_INFO_OLD = "group-info";
@@ -29,6 +31,12 @@ export const isFirstComponentGallery = (
29
31
  return isGallery(components?.[0]);
30
32
  };
31
33
 
34
+ export const isLastComponentGallery = (
35
+ components: ZappUIComponent[]
36
+ ): boolean => {
37
+ return isGallery(last(components));
38
+ };
39
+
32
40
  export const isGroup = (item): boolean => item?.component_type === GROUP;
33
41
 
34
42
  export const isEmptyGroup = (item): boolean =>
@@ -123,6 +123,12 @@ function getPlayerConfiguration({ platform, version }) {
123
123
  key: "skip_button_localization_text_skip_intro",
124
124
  initial_value: "Skip Intro",
125
125
  },
126
+ {
127
+ type: "text_input",
128
+ label: "Stream error message",
129
+ key: "stream_error_message",
130
+ initial_value: "Cannot play stream",
131
+ },
126
132
  {
127
133
  type: "text_input",
128
134
  label: "Locked message",
@@ -707,22 +713,6 @@ function getPlayerConfiguration({ platform, version }) {
707
713
  }
708
714
 
709
715
  if (isMobile(platform)) {
710
- localizations.fields.push(
711
- {
712
- type: "text_input",
713
- label: "Restrict playback on mobile networks alert title",
714
- key: "mobile_connection_restricted_alert_title",
715
- initial_value: "Restricted Connection Type",
716
- },
717
- {
718
- type: "text_input",
719
- label: "Restrict playback on mobile networks alert message",
720
- key: "mobile_connection_restricted_alert_message",
721
- initial_value:
722
- "This content can only be viewed over a Wi-Fi or LAN network.",
723
- }
724
- );
725
-
726
716
  general.fields.push(
727
717
  {
728
718
  section: "Default Timestamp Type",
@@ -11,16 +11,6 @@ describe("toNumber", () => {
11
11
  });
12
12
  });
13
13
 
14
- it("return number if input is BigInt", () => {
15
- const inputs = [5n, -5n];
16
- expect.assertions(inputs.length);
17
-
18
- inputs.forEach((input) => {
19
- const output = toNumber(input);
20
- expect(output).toBe(Number(input));
21
- });
22
- });
23
-
24
14
  it("return number if input is string as number", () => {
25
15
  const inputs = ["-1", "0", "1", "100", "0.2"];
26
16
  expect.assertions(inputs.length);
@@ -34,6 +24,8 @@ describe("toNumber", () => {
34
24
  it("return undefined if input is not a number", () => {
35
25
  const inputs = [
36
26
  "vfdvf",
27
+ "5n",
28
+ "-5n",
37
29
  null,
38
30
  undefined,
39
31
  NaN,
@@ -42,8 +34,6 @@ describe("toNumber", () => {
42
34
  { test: 1 },
43
35
  [],
44
36
  [1],
45
- "5n",
46
- "-5n",
47
37
  ];
48
38
 
49
39
  expect.assertions(inputs.length);
@@ -53,4 +43,29 @@ describe("toNumber", () => {
53
43
  expect(output).toBeUndefined();
54
44
  });
55
45
  });
46
+
47
+ describe("BigInt support", () => {
48
+ // Conditional test based on BigInt availability
49
+ const isBigIntSupported = typeof BigInt !== "undefined";
50
+
51
+ if (isBigIntSupported) {
52
+ it("converts BigInt to number when BigInt is supported", () => {
53
+ expect(toNumber(BigInt(5))).toBe(5);
54
+ expect(toNumber(BigInt(0))).toBe(0);
55
+ expect(toNumber(BigInt(-10))).toBe(-10);
56
+ expect(toNumber(BigInt(1000))).toBe(1000);
57
+ });
58
+
59
+ it("handles large BigInt values that may lose precision", () => {
60
+ const largeBigInt = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1);
61
+ const result = toNumber(largeBigInt);
62
+ expect(result).toBe(Number(largeBigInt));
63
+ });
64
+ } else {
65
+ it("skips BigInt tests when BigInt is not supported", () => {
66
+ // Placeholder test to indicate BigInt is not available
67
+ expect(typeof BigInt).toBe("undefined");
68
+ });
69
+ }
70
+ });
56
71
  });
@@ -156,10 +156,38 @@ describe("toPositiveNumber", () => {
156
156
  it("handles Symbol values", () => {
157
157
  expect(toPositiveNumber(Symbol("test"))).toBeUndefined();
158
158
  });
159
+ });
159
160
 
160
- it("converts BigInt values", () => {
161
- expect(toPositiveNumber(5n)).toBe(5);
162
- expect(toPositiveNumber(-5n)).toBeUndefined();
163
- });
161
+ describe("BigInt support", () => {
162
+ // Conditional test based on BigInt availability
163
+ const isBigIntSupported = typeof BigInt !== "undefined";
164
+
165
+ if (isBigIntSupported) {
166
+ it("converts positive BigInt values to numbers when BigInt is supported", () => {
167
+ expect(toPositiveNumber(BigInt(5))).toBe(5);
168
+ expect(toPositiveNumber(BigInt(100))).toBe(100);
169
+ expect(toPositiveNumber(BigInt(1000))).toBe(1000);
170
+ });
171
+
172
+ it("returns undefined for zero BigInt", () => {
173
+ expect(toPositiveNumber(BigInt(0))).toBeUndefined();
174
+ });
175
+
176
+ it("returns undefined for negative BigInt values", () => {
177
+ expect(toPositiveNumber(BigInt(-5))).toBeUndefined();
178
+ expect(toPositiveNumber(BigInt(-100))).toBeUndefined();
179
+ });
180
+
181
+ it("handles large positive BigInt values", () => {
182
+ const largeBigInt = BigInt(Number.MAX_SAFE_INTEGER);
183
+ const result = toPositiveNumber(largeBigInt);
184
+ expect(result).toBe(Number(largeBigInt));
185
+ });
186
+ } else {
187
+ it("skips BigInt tests when BigInt is not supported", () => {
188
+ // Placeholder test to indicate BigInt is not available
189
+ expect(typeof BigInt).toBe("undefined");
190
+ });
191
+ }
164
192
  });
165
193
  });
@@ -8,7 +8,11 @@ export const toNumber = (value: unknown): number | undefined => {
8
8
  return undefined;
9
9
  }
10
10
 
11
- if (R.is(Number, value) || R.is(BigInt, value) || R.is(String, value)) {
11
+ // Feature-detect BigInt support to avoid ReferenceError on older runtimes
12
+ const isBigIntSupported = typeof BigInt !== "undefined";
13
+ const isBigIntValue = isBigIntSupported && R.is(BigInt, value);
14
+
15
+ if (R.is(Number, value) || isBigIntValue || R.is(String, value)) {
12
16
  const numberOrNan = Number(value);
13
17
 
14
18
  return Number.isNaN(numberOrNan) ? undefined : numberOrNan;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "15.0.0-alpha.4200413542",
3
+ "version": "15.0.0-alpha.4225160176",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,11 +27,11 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "15.0.0-alpha.4200413542",
30
+ "@applicaster/applicaster-types": "15.0.0-alpha.4225160176",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
34
- "handlebars": "4.7.8",
34
+ "handlebars": "4.7.9",
35
35
  "memoizee": "0.4.15",
36
36
  "prop-types": "^15.0.0"
37
37
  },
@@ -1,5 +1,3 @@
1
- export { useFeedRefresh, feedRefreshLogger } from "./useFeedRefresh";
2
-
3
1
  export { useFeedLoader } from "./useFeedLoader";
4
2
 
5
3
  export { getSearchContext, getInflatedDataSourceUrl } from "./useInflatedUrl";
@@ -45,7 +45,7 @@ interface GetInflatedDataSourceUrl {
45
45
  (properties: {
46
46
  source?: string;
47
47
  contexts: {
48
- entry?: ZappEntry;
48
+ entry?: ZappEntry & { parent?: ZappEntry };
49
49
  screen?: ZappRiver;
50
50
  search?: object | string;
51
51
  };
@@ -1,6 +1,8 @@
1
1
  /// <reference types="@applicaster/applicaster-types" />
2
2
  import * as React from "react";
3
3
 
4
+ import { coreLogger } from "@applicaster/zapp-react-native-utils/logger";
5
+
4
6
  import { findComponentByType } from "@applicaster/zapp-react-native-utils/pluginUtils";
5
7
  import {
6
8
  usePlugins,
@@ -23,7 +25,7 @@ export function useComponentResolver(
23
25
 
24
26
  const components = useAppSelector(selectComponents);
25
27
 
26
- return React.useMemo(
28
+ const component = React.useMemo(
27
29
  () =>
28
30
  findComponentByType({
29
31
  components,
@@ -33,4 +35,12 @@ export function useComponentResolver(
33
35
  }),
34
36
  watchers
35
37
  );
38
+
39
+ if (!component) {
40
+ coreLogger.warn({
41
+ message: `useComponentResolver: Component of type ${componentType} not found`,
42
+ });
43
+ }
44
+
45
+ return component;
36
46
  }