@applicaster/zapp-react-native-utils 16.0.0-alpha.2434153375 → 16.0.0-alpha.2500443242

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 (97) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/ActionExecutor.ts +10 -1
  3. package/actionsExecutor/ActionExecutorContext.tsx +29 -1
  4. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  5. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +3 -0
  6. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  7. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -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 +201 -1
  11. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  12. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  13. package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
  14. package/actionsExecutor/actions/dismissBottomSheet.ts +2 -1
  15. package/actionsExecutor/actions/goBack.ts +60 -0
  16. package/actionsExecutor/actions/goHome.ts +31 -0
  17. package/actionsExecutor/actions/index.ts +8 -0
  18. package/actionsExecutor/actions/navigateToScreen.ts +29 -45
  19. package/actionsExecutor/actions/openBottomSheet.ts +35 -16
  20. package/actionsExecutor/actions/presentScreen.ts +78 -0
  21. package/actionsExecutor/actions/showAlert.ts +51 -0
  22. package/actionsExecutor/actions/showToast.ts +2 -2
  23. package/actionsExecutor/consts.ts +4 -0
  24. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +4 -11
  25. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  26. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +9 -2
  27. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +15 -2
  28. package/appUtils/playerManager/OverlayObserver/utils.ts +2 -5
  29. package/appUtils/playerManager/__tests__/playerNative.test.ts +76 -0
  30. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  31. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +97 -0
  32. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  33. package/appUtils/playerManager/const.ts +13 -0
  34. package/appUtils/playerManager/index.ts +7 -0
  35. package/appUtils/playerManager/player.ts +10 -0
  36. package/appUtils/playerManager/playerNative.ts +22 -0
  37. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  38. package/appUtils/playerManager/playerToolbox.ts +157 -0
  39. package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
  40. package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
  41. package/arrayUtils/index.ts +26 -43
  42. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  43. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  44. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  45. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  46. package/cellUtils/index.ts +74 -19
  47. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  48. package/configurationUtils/__tests__/modalBlocksParser.test.ts +286 -1
  49. package/configurationUtils/index.ts +77 -73
  50. package/configurationUtils/manifestKeyParser.ts +12 -1
  51. package/configurationUtils/modalBlocksParser.ts +78 -4
  52. package/entryActions/__tests__/buildEntryActions.test.ts +97 -18
  53. package/entryActions/aliasPresentation.ts +26 -42
  54. package/entryActions/buildEntryActions.ts +1 -0
  55. package/entryActions/index.ts +0 -2
  56. package/focusManager/FocusManager.ts +26 -0
  57. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  58. package/localizationUtils/index.ts +12 -6
  59. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  60. package/logger/xrayToolbox.ts +67 -0
  61. package/manifestUtils/_internals/index.js +8 -3
  62. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  63. package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
  64. package/manifestUtils/keys.js +6 -1
  65. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  66. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  67. package/manifestUtils/mobileAction/groups/defaults.js +2 -1
  68. package/modalState/ModalOrchestrator.ts +22 -9
  69. package/modalState/RemoteEditableCollection.ts +9 -1
  70. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +16 -0
  71. package/modalState/__tests__/RemoteEditableCollection.test.ts +90 -1
  72. package/modalState/__tests__/index.test.ts +30 -1
  73. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  74. package/modalState/index.ts +6 -1
  75. package/modalState/store.ts +9 -2
  76. package/modalState/types.ts +31 -2
  77. package/modalState/useBottomSheetContent.ts +24 -17
  78. package/navigationUtils/xrayNavigator.ts +12 -0
  79. package/package.json +2 -2
  80. package/perfTracker.ts +206 -0
  81. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  82. package/pluginUtils/index.ts +29 -0
  83. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  84. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  85. package/reactHooks/cell-click/index.ts +21 -14
  86. package/reactHooks/connection/index.tsx +8 -5
  87. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  88. package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
  89. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  90. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  91. package/reactHooks/navigation/index.ts +2 -0
  92. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  93. package/reactHooks/navigation/useRoute.ts +22 -5
  94. package/reactHooks/usePluginConfiguration.ts +84 -46
  95. package/riverComponetsMeasurementProvider/index.tsx +8 -1
  96. package/uiActionsRegistrator/__tests__/subscribe.test.ts +26 -0
  97. package/uiActionsRegistrator/registry.ts +16 -0
package/perfTracker.ts ADDED
@@ -0,0 +1,206 @@
1
+ import { createLogger } from "./logger";
2
+ import { NativeModules } from "react-native";
3
+
4
+ const { ProfilerBridge } = NativeModules;
5
+
6
+ const loggerObj = createLogger({
7
+ category: "PerfTracker",
8
+ subsystem: "PerfTelemetry",
9
+ });
10
+
11
+ const renderCounts: Record<string, number> = {};
12
+ const cellRenderCounts: Record<string, number> = {};
13
+
14
+ const timingStats: Record<
15
+ string,
16
+ { totalMs: number; count: number; maxMs: number; minMs: number }
17
+ > = {};
18
+
19
+ const t0 = Date.now();
20
+
21
+ /** True only where the native profiler is registered; lets always-on logs skip the perf lines without it. */
22
+ export const isProfilerAvailable = (): boolean => Boolean(ProfilerBridge);
23
+
24
+ export function getDelta(): number {
25
+ return Date.now() - t0;
26
+ }
27
+
28
+ export function recordTiming(
29
+ name: string,
30
+ durationMs: number,
31
+ details?: Record<string, any>
32
+ ): void {
33
+ const rounded = Math.round(durationMs * 100) / 100;
34
+
35
+ if (!timingStats[name]) {
36
+ timingStats[name] = {
37
+ totalMs: rounded,
38
+ count: 1,
39
+ maxMs: rounded,
40
+ minMs: rounded,
41
+ };
42
+ } else {
43
+ const s = timingStats[name];
44
+ s.totalMs = Math.round((s.totalMs + rounded) * 100) / 100;
45
+ s.count += 1;
46
+ if (rounded > s.maxMs) s.maxMs = rounded;
47
+ if (rounded < s.minMs) s.minMs = rounded;
48
+ }
49
+
50
+ const stat = timingStats[name];
51
+ const avg = Math.round((stat.totalMs / stat.count) * 100) / 100;
52
+ const delta = getDelta();
53
+
54
+ if (ProfilerBridge?.updateTimer) {
55
+ try {
56
+ ProfilerBridge.updateTimer("timings", name, rounded);
57
+ } catch (e) {}
58
+ }
59
+
60
+ if (stat.count <= 3 || rounded >= 3 || stat.count % 10 === 0) {
61
+ const msg = `[PERF TIMING #${stat.count}] ${name}: ${rounded}ms (avg: ${avg}ms, max: ${stat.maxMs}ms, total: ${stat.totalMs}ms) (+${delta}ms)`;
62
+
63
+ loggerObj.log_info(msg, {
64
+ timingName: name,
65
+ durationMs: rounded,
66
+ avgMs: avg,
67
+ maxMs: stat.maxMs,
68
+ minMs: stat.minMs,
69
+ totalMs: stat.totalMs,
70
+ count: stat.count,
71
+ deltaMs: delta,
72
+ ...(details || {}),
73
+ });
74
+ }
75
+ }
76
+
77
+ export function measureSync<T>(
78
+ name: string,
79
+ fn: () => T,
80
+ details?: Record<string, any>
81
+ ): T {
82
+ const start =
83
+ typeof performance !== "undefined" && performance.now
84
+ ? performance.now()
85
+ : Date.now();
86
+
87
+ try {
88
+ return fn();
89
+ } finally {
90
+ const end =
91
+ typeof performance !== "undefined" && performance.now
92
+ ? performance.now()
93
+ : Date.now();
94
+
95
+ recordTiming(name, end - start, details);
96
+ }
97
+ }
98
+
99
+ export function startTimer(
100
+ name: string,
101
+ details?: Record<string, any>
102
+ ): (additionalDetails?: Record<string, any>) => number {
103
+ const start =
104
+ typeof performance !== "undefined" && performance.now
105
+ ? performance.now()
106
+ : Date.now();
107
+
108
+ return (additionalDetails?: Record<string, any>) => {
109
+ const end =
110
+ typeof performance !== "undefined" && performance.now
111
+ ? performance.now()
112
+ : Date.now();
113
+
114
+ const duration = end - start;
115
+
116
+ recordTiming(name, duration, {
117
+ ...(details || {}),
118
+ ...(additionalDetails || {}),
119
+ });
120
+
121
+ return duration;
122
+ };
123
+ }
124
+
125
+ export function countRender(
126
+ componentName: string,
127
+ details?: Record<string, any>
128
+ ): number {
129
+ renderCounts[componentName] = (renderCounts[componentName] || 0) + 1;
130
+ const count = renderCounts[componentName];
131
+ const delta = getDelta();
132
+
133
+ if (ProfilerBridge?.updateCounter) {
134
+ try {
135
+ ProfilerBridge.updateCounter("render_count", componentName, count);
136
+ } catch (e) {}
137
+ }
138
+
139
+ const msg = `[PERF RENDER #${count}] ${componentName} (+${delta}ms)`;
140
+
141
+ loggerObj.log_info(msg, {
142
+ component: componentName,
143
+ count,
144
+ deltaMs: delta,
145
+ ...(details || {}),
146
+ });
147
+
148
+ return count;
149
+ }
150
+
151
+ export function countCellRender(
152
+ cellType: string,
153
+ itemId: string | number,
154
+ details?: Record<string, any>
155
+ ): number {
156
+ const key = `${cellType}:${itemId || "unknown"}`;
157
+ cellRenderCounts[key] = (cellRenderCounts[key] || 0) + 1;
158
+ const count = cellRenderCounts[key];
159
+
160
+ renderCounts[cellType] = (renderCounts[cellType] || 0) + 1;
161
+ const totalForType = renderCounts[cellType];
162
+ const delta = getDelta();
163
+
164
+ if (ProfilerBridge?.updateCounter) {
165
+ try {
166
+ ProfilerBridge.updateCounter("cell_render_count", cellType, totalForType);
167
+ } catch (e) {}
168
+ }
169
+
170
+ if (count > 1 || totalForType <= 12 || totalForType % 10 === 0) {
171
+ const msg = `[PERF CELL ${
172
+ count > 1 ? "RE-RENDER" : "RENDER"
173
+ } #${count}] ${cellType} item:${itemId} (total ${cellType}: #${totalForType}) (+${delta}ms)`;
174
+
175
+ loggerObj.log_info(msg, {
176
+ cellType,
177
+ itemId,
178
+ itemRenderCount: count,
179
+ totalForCellType: totalForType,
180
+ deltaMs: delta,
181
+ ...(details || {}),
182
+ });
183
+ }
184
+
185
+ return count;
186
+ }
187
+
188
+ export function dumpSummary(checkpoint: string): void {
189
+ const delta = getDelta();
190
+ let totalRenders = 0;
191
+
192
+ for (const k in renderCounts) {
193
+ totalRenders += renderCounts[k];
194
+ }
195
+
196
+ const summaryMsg = `=== [PERF SUMMARY at ${checkpoint}] total renders: ${totalRenders} (+${delta}ms) ===`;
197
+
198
+ loggerObj.log_info(summaryMsg, {
199
+ checkpoint,
200
+ elapsedMs: delta,
201
+ totalRenders,
202
+ renderCounts,
203
+ cellRenderCounts,
204
+ timingStats,
205
+ });
206
+ }
@@ -1,3 +1,9 @@
1
+ const mockAppStoreGet = jest.fn();
2
+
3
+ jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
4
+ appStore: { get: (key) => mockAppStoreGet(key) },
5
+ }));
6
+
1
7
  import * as R from "ramda";
2
8
  import * as pluginUtils from "../index";
3
9
 
@@ -148,6 +154,32 @@ describe("findPluginByIdentifier", () => {
148
154
  });
149
155
  });
150
156
 
157
+ describe("getPluginByIdentifier", () => {
158
+ const { getPluginByIdentifier } = pluginUtils;
159
+
160
+ const plugins = {
161
+ foo: pluginMock("foo", "foo", "some_plugin_type"),
162
+ bar: pluginMock("foo", "bar", "some_plugin_type"),
163
+ };
164
+
165
+ beforeEach(() => {
166
+ mockAppStoreGet.mockReset();
167
+ });
168
+
169
+ it("reads plugins from appStore and returns the matching plugin", () => {
170
+ mockAppStoreGet.mockReturnValue(plugins);
171
+
172
+ expect(getPluginByIdentifier("foo")).toEqual(plugins.foo);
173
+ expect(mockAppStoreGet).toHaveBeenCalledWith("plugins");
174
+ });
175
+
176
+ it("returns undefined when the plugin is not found", () => {
177
+ mockAppStoreGet.mockReturnValue(plugins);
178
+
179
+ expect(getPluginByIdentifier("do_not_exist")).toBeUndefined();
180
+ });
181
+ });
182
+
151
183
  describe("findPluginByType", () => {
152
184
  const { findPluginByType } = pluginUtils;
153
185
 
@@ -1,4 +1,5 @@
1
1
  import * as R from "ramda";
2
+ import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
2
3
  import { toCamelCase, toPascalCase } from "../stringUtils";
3
4
 
4
5
  import { getNavigationType } from "../navigationUtils";
@@ -97,6 +98,34 @@ export function findPluginByIdentifier(
97
98
  return plugin;
98
99
  }
99
100
 
101
+ /**
102
+ * Like findPluginByIdentifier, but reads the current plugins list from appStore.
103
+ * Convenience for non-render / hook code that doesn't already have `plugins` in hand.
104
+ * @param identifier of the plugin to look for
105
+ * @param doNotWarn if set to true, will not log a warning if plugin is not found
106
+ * @return whole plugin if found, undefined otherwise
107
+ */
108
+ export function getPluginByIdentifier(
109
+ identifier: string,
110
+ doNotWarn: boolean = false
111
+ ): (QuickBrickPlugin | ZappPlugin | Plugin) | undefined {
112
+ try {
113
+ const plugins = appStore.get("plugins") ?? {};
114
+
115
+ return findPluginByIdentifier(identifier, plugins, doNotWarn);
116
+ } catch (error) {
117
+ if (!doNotWarn) {
118
+ pluginUtilsLogger.warning({
119
+ message:
120
+ "Could not read plugins from appStore while resolving plugin by identifier",
121
+ data: { identifier, error },
122
+ });
123
+ }
124
+
125
+ return undefined;
126
+ }
127
+ }
128
+
100
129
  const pluginTypeForItemTypeMap = {
101
130
  playable: "player",
102
131
  };
@@ -0,0 +1,82 @@
1
+ import { renderHook } from "@testing-library/react-native";
2
+
3
+ const plugin = {
4
+ identifier: "cast",
5
+ configuration: {
6
+ title: "Configured",
7
+ localizations: { fr: { close: "Fermer" } },
8
+ },
9
+ };
10
+
11
+ jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
12
+ usePlugins: () => [plugin],
13
+ }));
14
+
15
+ jest.mock(
16
+ "@applicaster/zapp-react-native-utils/appUtils/localizationsHelper",
17
+ () => ({ getUILanguage: () => "fr" })
18
+ );
19
+
20
+ import {
21
+ getPluginConfiguration,
22
+ mergeLocalizations,
23
+ usePluginConfiguration,
24
+ } from "../usePluginConfiguration";
25
+
26
+ const manifest = {
27
+ custom_configuration_fields: [
28
+ { key: "title", initial_value: "Default" },
29
+ { key: "color", initial_value: "red" },
30
+ ],
31
+ localizations: {
32
+ fields: [
33
+ { key: "close", initial_value: "Close" },
34
+ { key: "cancel", initial_value: "Cancel" },
35
+ ],
36
+ },
37
+ };
38
+
39
+ it("matches usePluginConfiguration outside React", () => {
40
+ const { result } = renderHook(() => usePluginConfiguration("cast", manifest));
41
+
42
+ expect(getPluginConfiguration(plugin, manifest)).toEqual(result.current);
43
+ });
44
+
45
+ // Expected values are what R.mergeDeepWith((l, r) => (isNil(l) ? r : l)) returned before ramda was removed
46
+ describe("mergeLocalizations", () => {
47
+ const defaults = { en: { close: "Close", cancel: "Cancel" } };
48
+
49
+ it("returns the defaults when nothing is configured", () => {
50
+ expect(mergeLocalizations(undefined, defaults)).toEqual(defaults);
51
+ });
52
+
53
+ it("keeps configured values, fills nil ones and missing keys from defaults", () => {
54
+ expect(
55
+ mergeLocalizations(
56
+ { en: { close: null, extra: "X" }, fr: { close: "Fermer" } },
57
+ defaults
58
+ )
59
+ ).toEqual({
60
+ en: { close: "Close", extra: "X", cancel: "Cancel" },
61
+ fr: { close: "Fermer" },
62
+ });
63
+ });
64
+
65
+ it("does not merge arrays", () => {
66
+ expect(
67
+ mergeLocalizations(
68
+ { en: { close: ["a"] } },
69
+ { en: { close: ["b"], cancel: "C" } }
70
+ )
71
+ ).toEqual({ en: { close: ["a"], cancel: "C" } });
72
+ });
73
+ });
74
+
75
+ it("keeps configured languages first, so the fallback language stays the configured one", () => {
76
+ const merged = mergeLocalizations(
77
+ { he: { close: "סגור" }, en: { close: "Close it" } },
78
+ { en: { close: "Close" } }
79
+ );
80
+
81
+ expect(Object.keys(merged)).toEqual(["he", "en"]);
82
+ });
@@ -5,6 +5,7 @@ import { ActionExecutorContext } from "@applicaster/zapp-react-native-utils/acti
5
5
 
6
6
  const mockNavigator = {
7
7
  getPathname: () => "pathnameMock",
8
+ currentRoute: "pathnameMock",
8
9
  push: jest.fn(),
9
10
  data: {},
10
11
  };
@@ -29,6 +30,7 @@ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen", () => ({
29
30
  ),
30
31
  useTargetScreenData: jest.fn(() => ({})),
31
32
  useCurrentScreenData: jest.fn(() => ({})),
33
+ useScreenContext: jest.fn(() => ({})),
32
34
  }));
33
35
 
34
36
  jest.mock("@applicaster/zapp-react-native-ui-components/Contexts", () => ({
@@ -66,6 +68,12 @@ const wrapper = ({ children }) => (
66
68
  </CellTapContext.Provider>
67
69
  );
68
70
 
71
+ const actionExecutorWrapper = ({ children }) => (
72
+ <ActionExecutorContext.Provider value={actionExecutor}>
73
+ {children}
74
+ </ActionExecutorContext.Provider>
75
+ );
76
+
69
77
  describe("useCellClick", () => {
70
78
  it("returns a function", () => {
71
79
  const { result } = renderHook(() => useCellClick(mockProps));
@@ -95,4 +103,60 @@ describe("useCellClick", () => {
95
103
 
96
104
  cleanup();
97
105
  });
106
+
107
+ it("does not push when the cell finishes a hook", async () => {
108
+ mockNavigator.push.mockClear();
109
+
110
+ const item = {
111
+ id: "profile-1",
112
+ type: { value: "feed" },
113
+ extensions: {
114
+ tap_actions: {
115
+ actions: [
116
+ { type: "sessionStorageSet" },
117
+ { type: "finishHook", options: { success: true } },
118
+ ],
119
+ },
120
+ },
121
+ };
122
+
123
+ const { result } = renderHook(() => useCellClick({ item }), {
124
+ wrapper: actionExecutorWrapper,
125
+ });
126
+
127
+ await result.current();
128
+
129
+ await waitFor(() => {
130
+ expect(actionExecutor.handleEntryActions).toHaveBeenCalled();
131
+ });
132
+
133
+ expect(mockNavigator.push).not.toHaveBeenCalled();
134
+ cleanup();
135
+ });
136
+
137
+ it("pushes after tap actions that do not finish a hook", async () => {
138
+ mockNavigator.push.mockClear();
139
+
140
+ const item = {
141
+ id: "episode-1",
142
+ type: { value: "feed" },
143
+ extensions: {
144
+ tap_actions: {
145
+ actions: [{ type: "sessionStorageSet" }],
146
+ },
147
+ },
148
+ };
149
+
150
+ const { result } = renderHook(() => useCellClick({ item }), {
151
+ wrapper: actionExecutorWrapper,
152
+ });
153
+
154
+ await result.current();
155
+
156
+ await waitFor(() => {
157
+ expect(mockNavigator.push).toHaveBeenCalled();
158
+ });
159
+
160
+ cleanup();
161
+ });
98
162
  });
@@ -83,6 +83,12 @@ export const useCellClick = ({
83
83
 
84
84
  logOnPress(selectedItem, pathname, component, onCellTap);
85
85
 
86
+ const tapActions = selectedItem?.extensions?.tap_actions?.actions;
87
+
88
+ const finishesHook =
89
+ Array.isArray(tapActions) &&
90
+ tapActions.some((action) => action?.type === "finishHook");
91
+
86
92
  if (selectedItem) {
87
93
  await actionExecutor?.handleEntryActions(selectedItem, {
88
94
  component,
@@ -101,20 +107,21 @@ export const useCellClick = ({
101
107
  url: selectedItem.link.href,
102
108
  });
103
109
  }
104
- } else {
105
- if (isFunction(onCellTap)) {
106
- onCellTap?.(selectedItem, _index);
107
- } else {
108
- if (currentRoute === pathname || pathname.includes("video-modal")) {
109
- const targetScreen = component?.data?.target;
110
-
111
- push(
112
- R.when(
113
- () => targetScreen,
114
- R.mergeLeft({ screen_type: targetScreen })
115
- )(selectedItem)
116
- );
117
- }
110
+ } else if (isFunction(onCellTap)) {
111
+ onCellTap?.(selectedItem, _index);
112
+ } else if (!finishesHook) {
113
+ // finishHook already completes the hook and navigates to the original
114
+ // target. Pushing the cell as well stacks a second copy of that screen
115
+ // (e.g. home/home after a profile selector hook).
116
+ if (currentRoute === pathname || pathname.includes("video-modal")) {
117
+ const targetScreen = component?.data?.target;
118
+
119
+ push(
120
+ R.when(
121
+ () => targetScreen,
122
+ R.mergeLeft({ screen_type: targetScreen })
123
+ )(selectedItem)
124
+ );
118
125
  }
119
126
  }
120
127
  },
@@ -4,6 +4,11 @@ import { NetInfoState } from "@react-native-community/netinfo";
4
4
  import { NetworkStatusContext } from "@applicaster/zapp-react-native-ui-components/Contexts/NetworkStatusContext";
5
5
  import { isOnline } from "@applicaster/quick-brick-core/App/NetworkStatusProvider/utils";
6
6
 
7
+ type ConnectionInfo = NetInfoState & {
8
+ deviceStatus: NetStatus;
9
+ stillOfflineAfterResumeCount?: number;
10
+ };
11
+
7
12
  /**
8
13
  * This hook returns connection type from RN NetInfo component or boolean for iConnected if justOnlineStatus flag is set
9
14
  * @param {boolean} justOnlineStatus - flag if to return boolean for isConnected
@@ -12,16 +17,14 @@ import { isOnline } from "@applicaster/quick-brick-core/App/NetworkStatusProvide
12
17
 
13
18
  export function useConnectionInfo(justOnlineStatus: true): boolean;
14
19
 
15
- export function useConnectionInfo(
16
- justOnlineStatus: false
17
- ): NetInfoState & { deviceStatus: NetStatus };
20
+ export function useConnectionInfo(justOnlineStatus: false): ConnectionInfo;
18
21
 
19
22
  export function useConnectionInfo(
20
23
  justOnlineStatus: boolean
21
- ): boolean | (NetInfoState & { deviceStatus: NetStatus }) {
24
+ ): boolean | ConnectionInfo {
22
25
  const connectionInfo = React.useContext(
23
26
  NetworkStatusContext
24
- ) as NetInfoState & { deviceStatus: NetStatus };
27
+ ) as ConnectionInfo;
25
28
 
26
29
  return justOnlineStatus ? isOnline(connectionInfo) === true : connectionInfo;
27
30
  }