@applicaster/zapp-react-native-utils 15.0.0-alpha.3642255793 → 15.0.0-alpha.3842304615

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 (78) hide show
  1. package/README.md +0 -6
  2. package/actionUtils/index.ts +7 -0
  3. package/actionsExecutor/ActionExecutorContext.tsx +83 -6
  4. package/appUtils/HooksManager/index.ts +35 -0
  5. package/appUtils/focusManager/treeDataStructure/Tree/__tests__/Tree.test.js +46 -0
  6. package/appUtils/focusManager/treeDataStructure/Tree/index.js +18 -18
  7. package/appUtils/focusManagerAux/utils/index.ts +12 -6
  8. package/appUtils/focusManagerAux/utils/utils.ios.ts +6 -3
  9. package/appUtils/localizationsHelper.ts +4 -0
  10. package/appUtils/playerManager/index.ts +9 -0
  11. package/appUtils/playerManager/player.ts +1 -1
  12. package/appUtils/playerManager/playerNative.ts +2 -1
  13. package/appUtils/playerManager/usePlayer.tsx +5 -3
  14. package/cellUtils/__tests__/cellUtils.test.ts +39 -0
  15. package/cellUtils/index.ts +11 -1
  16. package/componentsUtils/index.ts +8 -0
  17. package/dateUtils/__tests__/dayjs.test.ts +0 -3
  18. package/dateUtils/index.ts +2 -0
  19. package/manifestUtils/_internals/__tests__/index.test.js +41 -0
  20. package/manifestUtils/_internals/index.js +33 -0
  21. package/manifestUtils/defaultManifestConfigurations/player.js +6 -16
  22. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +49 -0
  23. package/manifestUtils/fieldUtils/index.js +54 -0
  24. package/manifestUtils/index.js +2 -0
  25. package/manifestUtils/keys.js +228 -0
  26. package/manifestUtils/mobileAction/button/__tests__/mobileActionButton.test.js +168 -0
  27. package/manifestUtils/mobileAction/button/index.js +140 -0
  28. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +102 -0
  29. package/manifestUtils/mobileAction/container/index.js +73 -0
  30. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +127 -0
  31. package/manifestUtils/mobileAction/groups/defaults.js +76 -0
  32. package/manifestUtils/mobileAction/groups/index.js +80 -0
  33. package/manifestUtils/platformIsTV.js +13 -0
  34. package/numberUtils/__tests__/toNumber.test.ts +27 -12
  35. package/numberUtils/__tests__/toPositiveNumber.test.ts +32 -4
  36. package/numberUtils/index.ts +5 -1
  37. package/package.json +3 -3
  38. package/pluginUtils/index.ts +4 -5
  39. package/reactHooks/analytics/__tests__/useSendAnalyticsOnPress.test.ts +537 -0
  40. package/reactHooks/casting/index.ts +1 -0
  41. package/reactHooks/casting/useIsCasting.tsx +57 -0
  42. package/reactHooks/cell-click/index.ts +2 -1
  43. package/reactHooks/dev/__tests__/useReRenderLog.test.ts +188 -0
  44. package/reactHooks/device/useIsTablet.tsx +14 -19
  45. package/reactHooks/events/index.ts +20 -0
  46. package/reactHooks/feed/index.ts +0 -2
  47. package/reactHooks/feed/useInflatedUrl.ts +1 -1
  48. package/reactHooks/index.ts +2 -0
  49. package/reactHooks/resolvers/useComponentResolver.ts +13 -3
  50. package/reactHooks/screen/__tests__/useCurrentScreenIsHook.test.ts +103 -0
  51. package/reactHooks/screen/__tests__/useCurrentScreenIsStartupHook.test.ts +94 -0
  52. package/reactHooks/screen/index.ts +4 -0
  53. package/reactHooks/screen/useCurrentScreenIsHook.ts +9 -0
  54. package/reactHooks/screen/useCurrentScreenIsStartupHook.ts +8 -0
  55. package/reactHooks/state/__tests__/useComponentScreenState.test.ts +246 -0
  56. package/reactHooks/state/index.ts +2 -0
  57. package/reactHooks/state/useComponentScreenState.ts +45 -0
  58. package/reactHooks/ui/__tests__/useFadeOutWhenBlurred.test.ts +580 -0
  59. package/rectUtils/__tests__/index.test.ts +549 -0
  60. package/rectUtils/index.ts +2 -2
  61. package/refreshUtils/RefreshCoordinator/__tests__/refreshCoordinator.test.ts +206 -0
  62. package/refreshUtils/RefreshCoordinator/index.ts +245 -0
  63. package/refreshUtils/RefreshCoordinator/utils/__tests__/getDataRefreshConfig.test.ts +104 -0
  64. package/refreshUtils/RefreshCoordinator/utils/index.ts +29 -0
  65. package/screenPickerUtils/__tests__/index.test.ts +333 -0
  66. package/screenPickerUtils/index.ts +5 -0
  67. package/screenUtils/index.ts +3 -0
  68. package/time/__tests__/BackgroundTimer.test.ts +156 -0
  69. package/time/__tests__/Timer.test.ts +236 -0
  70. package/typeGuards/__tests__/isString.test.ts +21 -0
  71. package/typeGuards/index.ts +4 -0
  72. package/utils/__tests__/clone.test.ts +158 -0
  73. package/utils/__tests__/path.test.ts +7 -0
  74. package/utils/clone.ts +7 -0
  75. package/utils/index.ts +2 -1
  76. package/reactHooks/componentsMap/index.ts +0 -55
  77. package/reactHooks/feed/__tests__/useFeedRefresh.test.tsx +0 -75
  78. package/reactHooks/feed/useFeedRefresh.tsx +0 -65
@@ -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.3642255793",
3
+ "version": "15.0.0-alpha.3842304615",
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.3642255793",
30
+ "@applicaster/applicaster-types": "15.0.0-alpha.3842304615",
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
  },
@@ -30,10 +30,7 @@ export function getComponentModule({
30
30
  components: ComponentsMap;
31
31
  plugins: Plugin[] | QuickBrickPlugin[];
32
32
  }) {
33
- const component = R.compose(
34
- R.prop(R.__, components),
35
- toPascalCase
36
- )(componentType);
33
+ const component = components[toPascalCase(componentType)];
37
34
 
38
35
  if (component) {
39
36
  return component;
@@ -60,7 +57,9 @@ export function findComponentByType({
60
57
  ? R.compose(...R.reverse(decorators))
61
58
  : decorators;
62
59
 
63
- const component = getComponentModule({ componentType, components, plugins });
60
+ const module = getComponentModule({ componentType, components, plugins });
61
+
62
+ const component = module?.Component || module;
64
63
 
65
64
  return R.unless(R.isNil, applyDecorators)(component);
66
65
  }
@@ -0,0 +1,537 @@
1
+ import { renderHook, act } from "@testing-library/react-native";
2
+ import { useSendAnalyticsOnPress } from "../index";
3
+
4
+ // Mock dependencies
5
+ jest.mock("../../../analyticsUtils", () => ({
6
+ useAnalytics: jest.fn(),
7
+ }));
8
+
9
+ jest.mock(
10
+ "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks",
11
+ () => ({
12
+ useSendAnalyticsEventWithFunction: jest.fn(),
13
+ })
14
+ );
15
+
16
+ import { useAnalytics } from "../../../analyticsUtils";
17
+ import { useSendAnalyticsEventWithFunction } from "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks";
18
+
19
+ describe("useSendAnalyticsOnPress", () => {
20
+ const mockUseAnalytics = useAnalytics as jest.MockedFunction<
21
+ typeof useAnalytics
22
+ >;
23
+
24
+ const mockUseSendAnalyticsEventWithFunction =
25
+ useSendAnalyticsEventWithFunction as jest.MockedFunction<
26
+ typeof useSendAnalyticsEventWithFunction
27
+ >;
28
+
29
+ let mockSendOnClickEvent: jest.Mock;
30
+ let mockCallbackSendAnalyticsEvent: jest.Mock;
31
+
32
+ beforeEach(() => {
33
+ jest.clearAllMocks();
34
+
35
+ mockSendOnClickEvent = jest.fn();
36
+ mockCallbackSendAnalyticsEvent = jest.fn();
37
+
38
+ mockUseAnalytics.mockReturnValue({
39
+ sendOnClickEvent: mockSendOnClickEvent,
40
+ } as any);
41
+
42
+ mockUseSendAnalyticsEventWithFunction.mockReturnValue(
43
+ mockCallbackSendAnalyticsEvent
44
+ );
45
+ });
46
+
47
+ describe("initialization", () => {
48
+ it("should return a callback function", () => {
49
+ const item = { id: "item-1" };
50
+ const component = { type: "button" };
51
+ const zappPipesData = { key: "value" };
52
+
53
+ const { result } = renderHook(() =>
54
+ useSendAnalyticsOnPress(item, component, zappPipesData)
55
+ );
56
+
57
+ expect(typeof result.current).toBe("function");
58
+ });
59
+
60
+ it("should call useAnalytics with correct parameters", () => {
61
+ const item = { id: "item-1", title: "Item 1" };
62
+ const component = { type: "cell", name: "MasterCell" };
63
+ const zappPipesData = { feeds: [], screens: [] };
64
+
65
+ renderHook(() => useSendAnalyticsOnPress(item, component, zappPipesData));
66
+
67
+ expect(mockUseAnalytics).toHaveBeenCalledWith({
68
+ item,
69
+ component,
70
+ zappPipesData,
71
+ });
72
+ });
73
+
74
+ it("should call useSendAnalyticsEventWithFunction", () => {
75
+ const item = { id: "item-2" };
76
+ const component = { type: "list" };
77
+ const zappPipesData = {};
78
+
79
+ renderHook(() => useSendAnalyticsOnPress(item, component, zappPipesData));
80
+
81
+ expect(mockUseSendAnalyticsEventWithFunction).toHaveBeenCalled();
82
+ });
83
+ });
84
+
85
+ describe("callback execution", () => {
86
+ it("should call analytics event when callback is invoked", () => {
87
+ const item = { id: "item-1" };
88
+ const component = { type: "button" };
89
+ const zappPipesData = { key: "value" };
90
+
91
+ const { result } = renderHook(() =>
92
+ useSendAnalyticsOnPress(item, component, zappPipesData)
93
+ );
94
+
95
+ const pressedItem = { id: "pressed-item" };
96
+ const index = 5;
97
+
98
+ act(() => {
99
+ result.current(pressedItem, index);
100
+ });
101
+
102
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith({
103
+ sendAnalyticsFunction: mockSendOnClickEvent,
104
+ extraProps: {
105
+ item: pressedItem,
106
+ index,
107
+ component,
108
+ zappPipesData,
109
+ },
110
+ });
111
+ });
112
+
113
+ it("should handle different items on each press", () => {
114
+ const item = { id: "item-1" };
115
+ const component = { type: "list" };
116
+ const zappPipesData = {};
117
+
118
+ const { result } = renderHook(() =>
119
+ useSendAnalyticsOnPress(item, component, zappPipesData)
120
+ );
121
+
122
+ const item1 = { id: "item-a", title: "Item A" };
123
+ const item2 = { id: "item-b", title: "Item B" };
124
+
125
+ act(() => {
126
+ result.current(item1, 0);
127
+ });
128
+
129
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenLastCalledWith(
130
+ expect.objectContaining({
131
+ extraProps: expect.objectContaining({
132
+ item: item1,
133
+ index: 0,
134
+ }),
135
+ })
136
+ );
137
+
138
+ act(() => {
139
+ result.current(item2, 1);
140
+ });
141
+
142
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenLastCalledWith(
143
+ expect.objectContaining({
144
+ extraProps: expect.objectContaining({
145
+ item: item2,
146
+ index: 1,
147
+ }),
148
+ })
149
+ );
150
+ });
151
+
152
+ it("should handle different indices", () => {
153
+ const item = { id: "item-1" };
154
+ const component = { type: "grid" };
155
+ const zappPipesData = {};
156
+
157
+ const { result } = renderHook(() =>
158
+ useSendAnalyticsOnPress(item, component, zappPipesData)
159
+ );
160
+
161
+ const pressedItem = { id: "pressed" };
162
+
163
+ for (let i = 0; i < 5; i++) {
164
+ act(() => {
165
+ result.current(pressedItem, i);
166
+ });
167
+
168
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenLastCalledWith(
169
+ expect.objectContaining({
170
+ extraProps: expect.objectContaining({
171
+ index: i,
172
+ }),
173
+ })
174
+ );
175
+ }
176
+ });
177
+
178
+ it("should always use the current component from closure", () => {
179
+ const item = { id: "item-1" };
180
+ const component = { type: "carousel", name: "MainCarousel" };
181
+ const zappPipesData = { feeds: [] };
182
+
183
+ const { result } = renderHook(() =>
184
+ useSendAnalyticsOnPress(item, component, zappPipesData)
185
+ );
186
+
187
+ act(() => {
188
+ result.current({ id: "test" }, 0);
189
+ });
190
+
191
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
192
+ expect.objectContaining({
193
+ extraProps: expect.objectContaining({
194
+ component,
195
+ }),
196
+ })
197
+ );
198
+ });
199
+
200
+ it("should always use the current zappPipesData from closure", () => {
201
+ const item = { id: "item-1" };
202
+ const component = { type: "button" };
203
+ const zappPipesData = { feeds: ["feed1"], screens: ["screen1"] };
204
+
205
+ const { result } = renderHook(() =>
206
+ useSendAnalyticsOnPress(item, component, zappPipesData)
207
+ );
208
+
209
+ act(() => {
210
+ result.current({ id: "test" }, 0);
211
+ });
212
+
213
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
214
+ expect.objectContaining({
215
+ extraProps: expect.objectContaining({
216
+ zappPipesData,
217
+ }),
218
+ })
219
+ );
220
+ });
221
+ });
222
+
223
+ describe("memoization", () => {
224
+ it("should memoize callback based on dependencies", () => {
225
+ const item = { id: "item-1" };
226
+ const component = { type: "button" };
227
+ const zappPipesData = { key: "value" };
228
+
229
+ const { result, rerender } = renderHook(
230
+ ({ comp, data }) => useSendAnalyticsOnPress(item, comp, data),
231
+ {
232
+ initialProps: {
233
+ comp: component,
234
+ data: zappPipesData,
235
+ },
236
+ }
237
+ );
238
+
239
+ const firstCallback = result.current;
240
+
241
+ // Rerender without changing dependencies
242
+ rerender({
243
+ comp: component,
244
+ data: zappPipesData,
245
+ });
246
+
247
+ const secondCallback = result.current;
248
+
249
+ // Callback should be memoized
250
+ expect(firstCallback).toBe(secondCallback);
251
+ });
252
+
253
+ it("should create new callback when component changes", () => {
254
+ const item = { id: "item-1" };
255
+ const component1 = { type: "button" };
256
+ const component2 = { type: "list" };
257
+ const zappPipesData = {};
258
+
259
+ const { result, rerender } = renderHook(
260
+ ({ comp }) => useSendAnalyticsOnPress(item, comp, zappPipesData),
261
+ {
262
+ initialProps: { comp: component1 },
263
+ }
264
+ );
265
+
266
+ const firstCallback = result.current;
267
+
268
+ rerender({ comp: component2 });
269
+
270
+ const secondCallback = result.current;
271
+
272
+ // Callback should be different when component changes
273
+ expect(firstCallback).not.toBe(secondCallback);
274
+ });
275
+
276
+ it("should create new callback when zappPipesData changes", () => {
277
+ const item = { id: "item-1" };
278
+ const component = { type: "button" };
279
+ const zappPipesData1 = { feeds: [] as string[] };
280
+ const zappPipesData2 = { feeds: ["feed1"] };
281
+
282
+ const { result, rerender } = renderHook(
283
+ ({ data }) => useSendAnalyticsOnPress(item, component, data),
284
+ {
285
+ initialProps: { data: zappPipesData1 },
286
+ }
287
+ );
288
+
289
+ const firstCallback = result.current;
290
+
291
+ rerender({ data: zappPipesData2 });
292
+
293
+ const secondCallback = result.current;
294
+
295
+ expect(firstCallback).not.toBe(secondCallback);
296
+ });
297
+
298
+ it("should create new callback when callbackSendAnalyticsEvent changes", () => {
299
+ const item = { id: "item-1" };
300
+ const component = { type: "button" };
301
+ const zappPipesData = {};
302
+
303
+ const callback1 = jest.fn();
304
+ const callback2 = jest.fn();
305
+
306
+ mockUseSendAnalyticsEventWithFunction
307
+ .mockReturnValueOnce(callback1)
308
+ .mockReturnValueOnce(callback2);
309
+
310
+ const { result, rerender } = renderHook(
311
+ ({ item, component, zappPipesData }) =>
312
+ useSendAnalyticsOnPress(item, component, zappPipesData),
313
+ {
314
+ initialProps: {
315
+ item,
316
+ component,
317
+ zappPipesData,
318
+ },
319
+ }
320
+ );
321
+
322
+ const firstCallback = result.current;
323
+
324
+ rerender({ item, component, zappPipesData });
325
+
326
+ const secondCallback = result.current;
327
+
328
+ // Even though we didn't change inputs, if the underlying callback changes,
329
+ // our callback should change too
330
+ expect(firstCallback).not.toBe(secondCallback);
331
+ });
332
+ });
333
+
334
+ describe("edge cases", () => {
335
+ it("should handle null item", () => {
336
+ const component = { type: "button" };
337
+ const zappPipesData = {};
338
+
339
+ const { result } = renderHook(() =>
340
+ useSendAnalyticsOnPress(null, component, zappPipesData)
341
+ );
342
+
343
+ expect(() => {
344
+ act(() => {
345
+ result.current({ id: "test" }, 0);
346
+ });
347
+ }).not.toThrow();
348
+ });
349
+
350
+ it("should handle undefined item", () => {
351
+ const component = { type: "button" };
352
+ const zappPipesData = {};
353
+
354
+ const { result } = renderHook(() =>
355
+ useSendAnalyticsOnPress(undefined, component, zappPipesData)
356
+ );
357
+
358
+ expect(() => {
359
+ act(() => {
360
+ result.current({ id: "test" }, 0);
361
+ });
362
+ }).not.toThrow();
363
+ });
364
+
365
+ it("should handle null component", () => {
366
+ const item = { id: "item-1" };
367
+ const zappPipesData = {};
368
+
369
+ const { result } = renderHook(() =>
370
+ useSendAnalyticsOnPress(item, null, zappPipesData)
371
+ );
372
+
373
+ act(() => {
374
+ result.current({ id: "test" }, 0);
375
+ });
376
+
377
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
378
+ expect.objectContaining({
379
+ extraProps: expect.objectContaining({
380
+ component: null,
381
+ }),
382
+ })
383
+ );
384
+ });
385
+
386
+ it("should handle null zappPipesData", () => {
387
+ const item = { id: "item-1" };
388
+ const component = { type: "button" };
389
+
390
+ const { result } = renderHook(() =>
391
+ useSendAnalyticsOnPress(item, component, null)
392
+ );
393
+
394
+ act(() => {
395
+ result.current({ id: "test" }, 0);
396
+ });
397
+
398
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
399
+ expect.objectContaining({
400
+ extraProps: expect.objectContaining({
401
+ zappPipesData: null,
402
+ }),
403
+ })
404
+ );
405
+ });
406
+
407
+ it("should handle zero index", () => {
408
+ const item = { id: "item-1" };
409
+ const component = { type: "list" };
410
+ const zappPipesData = {};
411
+
412
+ const { result } = renderHook(() =>
413
+ useSendAnalyticsOnPress(item, component, zappPipesData)
414
+ );
415
+
416
+ act(() => {
417
+ result.current({ id: "test" }, 0);
418
+ });
419
+
420
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
421
+ expect.objectContaining({
422
+ extraProps: expect.objectContaining({
423
+ index: 0,
424
+ }),
425
+ })
426
+ );
427
+ });
428
+
429
+ it("should handle negative index", () => {
430
+ const item = { id: "item-1" };
431
+ const component = { type: "list" };
432
+ const zappPipesData = {};
433
+
434
+ const { result } = renderHook(() =>
435
+ useSendAnalyticsOnPress(item, component, zappPipesData)
436
+ );
437
+
438
+ act(() => {
439
+ result.current({ id: "test" }, -1);
440
+ });
441
+
442
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
443
+ expect.objectContaining({
444
+ extraProps: expect.objectContaining({
445
+ index: -1,
446
+ }),
447
+ })
448
+ );
449
+ });
450
+
451
+ it("should handle complex item structures", () => {
452
+ const item = { id: "item-1" };
453
+ const component = { type: "cell" };
454
+ const zappPipesData = {};
455
+
456
+ const { result } = renderHook(() =>
457
+ useSendAnalyticsOnPress(item, component, zappPipesData)
458
+ );
459
+
460
+ const complexItem = {
461
+ id: "complex",
462
+ nested: {
463
+ deep: {
464
+ structure: true,
465
+ },
466
+ },
467
+ array: [1, 2, 3],
468
+ };
469
+
470
+ act(() => {
471
+ result.current(complexItem, 5);
472
+ });
473
+
474
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
475
+ expect.objectContaining({
476
+ extraProps: expect.objectContaining({
477
+ item: complexItem,
478
+ }),
479
+ })
480
+ );
481
+ });
482
+ });
483
+
484
+ describe("sendOnClickEvent integration", () => {
485
+ it("should pass sendOnClickEvent from useAnalytics", () => {
486
+ const item = { id: "item-1" };
487
+ const component = { type: "button" };
488
+ const zappPipesData = {};
489
+
490
+ const customSendOnClickEvent = jest.fn();
491
+
492
+ mockUseAnalytics.mockReturnValue({
493
+ sendOnClickEvent: customSendOnClickEvent,
494
+ } as any);
495
+
496
+ const { result } = renderHook(() =>
497
+ useSendAnalyticsOnPress(item, component, zappPipesData)
498
+ );
499
+
500
+ act(() => {
501
+ result.current({ id: "test" }, 0);
502
+ });
503
+
504
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
505
+ expect.objectContaining({
506
+ sendAnalyticsFunction: customSendOnClickEvent,
507
+ })
508
+ );
509
+ });
510
+
511
+ it("should handle missing sendOnClickEvent gracefully", () => {
512
+ const item = { id: "item-1" };
513
+ const component = { type: "button" };
514
+ const zappPipesData = {};
515
+
516
+ mockUseAnalytics.mockReturnValue({
517
+ sendOnClickEvent: undefined,
518
+ } as any);
519
+
520
+ const { result } = renderHook(() =>
521
+ useSendAnalyticsOnPress(item, component, zappPipesData)
522
+ );
523
+
524
+ expect(() => {
525
+ act(() => {
526
+ result.current({ id: "test" }, 0);
527
+ });
528
+ }).not.toThrow();
529
+
530
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
531
+ expect.objectContaining({
532
+ sendAnalyticsFunction: undefined,
533
+ })
534
+ );
535
+ });
536
+ });
537
+ });
@@ -0,0 +1 @@
1
+ export { useIsCasting } from "./useIsCasting";
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+ import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
3
+ import { useAppState } from "@applicaster/zapp-react-native-utils/reactHooks/app";
4
+ import { isApplePlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
5
+
6
+ const CHROMECAST_IDENTIFIER = "quick-brick-chromecast-action";
7
+
8
+ const fakeEntry: any = {
9
+ title: "Chromecast use is casting fake entry",
10
+ id: "quick-brick-use-is-casting-fake-entry",
11
+ };
12
+
13
+ /**
14
+ * Returns the cast action state connection status, does not update when app is in background on Apple platforms
15
+ * On other platforms, it continues to check the cast action state even when the app is in background
16
+ */
17
+ export const useIsCasting = () => {
18
+ const actionContext = useActions(CHROMECAST_IDENTIFIER);
19
+ const appState = useAppState();
20
+ const isAppInForeground = appState === "active";
21
+ const shouldOnlyUpdateInForeground = isApplePlatform();
22
+
23
+ const [castActionState, setCastActionState] = React.useState(
24
+ !actionContext ? null : actionContext.initialEntryState(fakeEntry)
25
+ );
26
+
27
+ React.useEffect(() => {
28
+ if (!shouldOnlyUpdateInForeground || isAppInForeground) {
29
+ if (actionContext && castActionState === null) {
30
+ setCastActionState(actionContext.initialEntryState(fakeEntry));
31
+ }
32
+ }
33
+ }, [
34
+ actionContext,
35
+ setCastActionState,
36
+ isAppInForeground,
37
+ shouldOnlyUpdateInForeground,
38
+ ]);
39
+
40
+ React.useEffect(() => {
41
+ if (typeof actionContext?.addListener === "function") {
42
+ return actionContext?.addListener(String(fakeEntry.id), (state) => {
43
+ // Only check foreground state on Apple platforms
44
+ if (!shouldOnlyUpdateInForeground || isAppInForeground) {
45
+ setCastActionState(state);
46
+ }
47
+ });
48
+ }
49
+ }, [
50
+ setCastActionState,
51
+ actionContext,
52
+ isAppInForeground,
53
+ shouldOnlyUpdateInForeground,
54
+ ]);
55
+
56
+ return !!castActionState?.connected;
57
+ };