@applicaster/zapp-react-native-ui-components 16.0.0-rc.9 → 16.0.0-rc.91

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 (146) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/Cell/Cell.tsx +3 -6
  5. package/Components/Cell/TvOSCellComponent.tsx +0 -1
  6. package/Components/CellRendererResolver/index.ts +2 -2
  7. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  8. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  9. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +54 -0
  10. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  11. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +188 -0
  16. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  17. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  18. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  19. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  20. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +90 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  22. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  23. package/Components/GeneralContentScreen/index.ts +2 -0
  24. package/Components/Layout/FullWidthRow.tsx +38 -0
  25. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +21 -6
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  29. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  30. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  31. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  32. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  33. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  34. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  35. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  36. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  37. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  38. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  49. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  50. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  51. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  52. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -2
  53. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  54. package/Components/MasterCell/dataAdapter.ts +15 -5
  55. package/Components/MasterCell/hooks/index.ts +2 -0
  56. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  57. package/Components/MasterCell/index.tsx +30 -5
  58. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  59. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  60. package/Components/MasterCell/utils/index.ts +85 -46
  61. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  62. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  63. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  64. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  65. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  66. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +392 -0
  67. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +728 -0
  68. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +628 -0
  69. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +982 -0
  70. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +94 -0
  71. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  72. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  73. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +248 -0
  74. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +386 -0
  75. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  76. package/Components/ModalComponent/BottomSheetModalContent.tsx +419 -50
  77. package/Components/ModalComponent/Button/index.tsx +5 -3
  78. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  79. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  80. package/Components/ModalComponent/Header/index.tsx +20 -7
  81. package/Components/ModalComponent/Header/utils.ts +1 -1
  82. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  83. package/Components/ModalComponent/SortableList.tsx +157 -0
  84. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  85. package/Components/ModalComponent/TextInputContent.tsx +112 -0
  86. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  87. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +270 -0
  88. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  89. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +156 -0
  90. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  91. package/Components/ModalComponent/index.tsx +18 -9
  92. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  93. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  94. package/Components/ModalComponent/presets/index.ts +27 -0
  95. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  96. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  97. package/Components/ModalComponent/presets/types.ts +35 -0
  98. package/Components/ModalComponent/showTextInput.ts +43 -0
  99. package/Components/ModalComponent/utils.ts +82 -21
  100. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  101. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  102. package/Components/OfflineHandler/hooks.ts +111 -0
  103. package/Components/OfflineHandler/index.tsx +51 -73
  104. package/Components/OfflineHandler/utils/index.ts +1 -13
  105. package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
  106. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  107. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  108. package/Components/PreloaderWrapper/index.tsx +1 -1
  109. package/Components/River/ComponentsMap/ComponentsMap.tsx +87 -61
  110. package/Components/River/River.tsx +4 -0
  111. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +140 -89
  112. package/Components/River/__tests__/componentsMap.test.js +8 -3
  113. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  114. package/Components/Screen/index.tsx +8 -4
  115. package/Components/Screen/navigationHandler.ts +8 -8
  116. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  117. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  118. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  119. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  120. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  121. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  122. package/Components/TopCutoffOverlay/index.tsx +2 -2
  123. package/Components/Transitioner/Transitioner.tsx +28 -17
  124. package/Components/VideoLive/LiveImageManager.ts +1 -1
  125. package/Components/VideoModal/utils.ts +6 -1
  126. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  127. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  128. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  129. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  130. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  131. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  132. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  133. package/Helpers/index.js +0 -39
  134. package/Hooks/useEntryActionsExpander.ts +63 -0
  135. package/package.json +6 -5
  136. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  137. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  138. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  139. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  140. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  141. package/Components/Screen/utils.ts +0 -16
  142. package/Helpers/Analytics/index.js +0 -95
  143. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  144. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  145. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  146. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -0,0 +1,337 @@
1
+ import * as React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+
4
+ jest.mock(
5
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager",
6
+ () => ({
7
+ playerManager: {
8
+ invokeHandler: jest.fn(),
9
+ getPluginConfiguration: jest.fn(() => ({})),
10
+ isRegistered: jest.fn(() => true),
11
+ on: jest.fn().mockReturnThis(),
12
+ removeHandler: jest.fn().mockReturnThis(),
13
+ getActivePlayer: jest.fn(() => null),
14
+ closeNativePlayer: jest.fn(),
15
+ },
16
+ })
17
+ );
18
+
19
+ jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
20
+ // Defaults to `false` for the hardware-back-handling tests below, which
21
+ // key off `isTV`. The ProgramInfo/content-context describe block below
22
+ // overrides this per test (`isTV()` is also what PlayerContainer's
23
+ // `playerContentValue` uses to pick TV vs mobile data keys, decoupled
24
+ // from `isTvOSPlatform` below).
25
+ isTV: jest.fn(() => false),
26
+ isAndroidTVPlatform: jest.fn(() => false),
27
+ // `isTvOSPlatform` backs a module-level constant in PlayerContainer.tsx that
28
+ // is read once at import time, so it must already be `true` here in order
29
+ // to exercise `renderApplePlayer`'s tvOS-only ProgramInfo branch in this
30
+ // test file. It does not affect the hardware-back-handling tests below,
31
+ // which key off `isTV` instead.
32
+ isTvOSPlatform: jest.fn(() => true),
33
+ isApplePlatform: jest.fn(() => true),
34
+ platformSelect: jest.fn(({ native }) => native),
35
+ }));
36
+
37
+ jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () => {
38
+ // Keep the real data-key resolver (createConfigValueGetter,
39
+ // resolvePlayerContentText, VIDEO_PLAYER_CONTENT_DATA_KEYS) so the tvOS
40
+ // ProgramInfo data-key tests exercise actual resolution logic, not a stub.
41
+ const actual = jest.requireActual(
42
+ "@applicaster/zapp-react-native-utils/playerUtils"
43
+ );
44
+
45
+ return {
46
+ ...actual,
47
+ isAudioItem: jest.fn(() => false),
48
+ isInlineTV: jest.fn(() => false),
49
+ };
50
+ });
51
+
52
+ jest.mock(
53
+ "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent",
54
+ () => ({
55
+ TVEventHandlerComponent: ({ children }) => children,
56
+ })
57
+ );
58
+
59
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => ({
60
+ usePrevious: jest.fn(),
61
+ }));
62
+
63
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => {
64
+ const navigator = {
65
+ closeVideoModal: jest.fn(),
66
+ minimiseVideoModal: jest.fn(),
67
+ maximiseVideoModal: jest.fn(),
68
+ fullscreenVideoModal: jest.fn(),
69
+ isVideoModalDocked: jest.fn(() => false),
70
+ goBack: jest.fn(),
71
+ replaceTop: jest.fn(),
72
+ push: jest.fn(),
73
+ setPlayNextOverlay: jest.fn(),
74
+ currentRoute: "/playable/entry-1",
75
+ };
76
+
77
+ const backHandlerRef: { current: (() => boolean) | null } = {
78
+ current: null,
79
+ };
80
+
81
+ return {
82
+ useBackHandler: jest.fn((cb) => {
83
+ backHandlerRef.current = cb;
84
+ }),
85
+ useNavigation: jest.fn(() => navigator),
86
+ __navigator: navigator,
87
+ __backHandlerRef: backHandlerRef,
88
+ };
89
+ });
90
+
91
+ jest.mock("@applicaster/zapp-react-native-bridge/QuickBrick", () => ({
92
+ QUICK_BRICK_EVENTS: {
93
+ IDLE_TIMER_DISABLED: "IDLE_TIMER_DISABLED",
94
+ MOVE_APP_TO_BACKGROUND: "MOVE_APP_TO_BACKGROUND",
95
+ },
96
+ sendQuickBrickEvent: jest.fn(),
97
+ }));
98
+
99
+ // Stubbed so PlayerContainer can render without ProgramInfo's own
100
+ // redux/style plumbing. It no longer needs to capture Title/Subtitle: those
101
+ // come from the player manager's content channel, and the key-set selection
102
+ // they depend on is covered directly in
103
+ // `playerManager/__tests__/playerContent.test.ts`.
104
+ jest.mock("../ProgramInfo", () => ({
105
+ ProgramInfo: () => null,
106
+ }));
107
+
108
+ jest.mock("../../AudioPlayer", () => ({
109
+ AudioPlayer: () => null,
110
+ }));
111
+
112
+ jest.mock("../logger", () => ({
113
+ log_debug: jest.fn(),
114
+ log_info: jest.fn(),
115
+ log_warning: jest.fn(),
116
+ playerContainerLogger: { error: jest.fn() },
117
+ }));
118
+
119
+ jest.mock(
120
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer",
121
+ () => {
122
+ const player = {
123
+ addListener: jest.fn(),
124
+ removeListener: jest.fn(),
125
+ isPaused: jest.fn(() => false),
126
+ isAd: jest.fn(() => false),
127
+ seekTo: jest.fn(),
128
+ getPluginConfiguration: jest.fn(() => ({})),
129
+ getOverlayObservable: jest.fn(() => ({ getPlayNextEntry: () => null })),
130
+ };
131
+
132
+ return {
133
+ usePlayer: jest.fn(() => player),
134
+ __player: player,
135
+ };
136
+ }
137
+ );
138
+
139
+ jest.mock(
140
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayNextOverlay",
141
+ () => ({
142
+ usePlayNextOverlay: jest.fn(() => null),
143
+ })
144
+ );
145
+
146
+ jest.mock(
147
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand",
148
+ () => ({
149
+ PlayerNativeCommandTypes: { clearPlayerData: "clearPlayerData" },
150
+ PlayerNativeSendCommand: jest.fn(),
151
+ })
152
+ );
153
+
154
+ jest.mock(
155
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext",
156
+ () => ({
157
+ useSetNavbarState: jest.fn(() => ({ setVisible: jest.fn() })),
158
+ })
159
+ );
160
+
161
+ jest.mock(
162
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useTargetScreenData",
163
+ () => ({
164
+ useTargetScreenData: jest.fn(() => ({ id: "screen-id", styles: {} })),
165
+ })
166
+ );
167
+
168
+ jest.mock("../PlayerContainerContext", () => {
169
+ const ReactActual = require("react");
170
+
171
+ const PlayerContainerContext = ReactActual.createContext({
172
+ isLanguageOverlayVisible: false,
173
+ bottomFocusableId: "bottom",
174
+ showComponentsContainer: false,
175
+ refs: null,
176
+ });
177
+
178
+ return {
179
+ PlayerContainerContext,
180
+ // Mirrors just enough of the real provider for these tests: renders an
181
+ // actual `PlayerContainerContext.Provider` so the real
182
+ // `PlayerContainerContext.Consumer` inside PlayerContainer.tsx sees the
183
+ // container fields a real provider would give it, instead of a bare
184
+ // `({ children }) => children` passthrough.
185
+ PlayerContainerContextProvider: ({ children }: any) =>
186
+ ReactActual.createElement(
187
+ PlayerContainerContext.Provider,
188
+ {
189
+ value: {
190
+ isLanguageOverlayVisible: false,
191
+ bottomFocusableId: "bottom",
192
+ showComponentsContainer: false,
193
+ refs: null,
194
+ },
195
+ },
196
+ children
197
+ ),
198
+ };
199
+ });
200
+
201
+ jest.mock(
202
+ "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup",
203
+ () => ({
204
+ FocusableGroup: ({ children }) => children,
205
+ })
206
+ );
207
+
208
+ jest.mock("../WappersView/PlayerFocusableWrapperView", () => ({
209
+ PlayerFocusableWrapperView: ({ children }) => children,
210
+ }));
211
+
212
+ jest.mock("../WappersView/ComponentFocusableWrapperView", () => ({
213
+ ComponentFocusableWrapperView: () => null,
214
+ }));
215
+
216
+ jest.mock("../index", () => ({
217
+ FocusableGroupMainContainerId: "player-container-general",
218
+ }));
219
+
220
+ jest.mock(
221
+ "@applicaster/zapp-react-native-utils/navigationUtils/itemTypeMatchers",
222
+ () => ({
223
+ isPlayable: jest.fn(() => true),
224
+ })
225
+ );
226
+
227
+ jest.mock("../../GeneralContentScreen", () => ({
228
+ GeneralContentScreen: () => null,
229
+ }));
230
+
231
+ jest.mock("@applicaster/zapp-react-native-redux", () => ({
232
+ useAppData: jest.fn(() => ({ isTabletPortrait: false })),
233
+ }));
234
+
235
+ import { PlayerContainer, VideoModalMode } from "../PlayerContainer";
236
+
237
+ const { __navigator: mockNavigator, __backHandlerRef: mockBackHandlerRef } =
238
+ jest.requireMock("@applicaster/zapp-react-native-utils/reactHooks");
239
+
240
+ const { playerManager: mockPlayerManager } = jest.requireMock(
241
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager"
242
+ );
243
+
244
+ const { __player: mockPlayer } = jest.requireMock(
245
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer"
246
+ );
247
+
248
+ const { isTV: mockIsTV } = jest.requireMock(
249
+ "@applicaster/zapp-react-native-utils/reactUtils"
250
+ );
251
+
252
+ // Renders children so that `renderApplePlayer`'s returned <ProgramInfo>
253
+ // element (passed as this component's `children` prop) actually mounts —
254
+ // otherwise React never invokes the ProgramInfo function component at all.
255
+ const Player = React.forwardRef((props: any, _ref) => props.children ?? null);
256
+
257
+ const item = {
258
+ id: "entry-1",
259
+ title: "Test entry",
260
+ summary: "",
261
+ content: { src: "https://example.com/video.m3u8" },
262
+ extensions: {},
263
+ } as any;
264
+
265
+ const renderPlayerContainer = (props = {}) =>
266
+ render(
267
+ <PlayerContainer
268
+ Player={Player}
269
+ item={item}
270
+ style={{}}
271
+ loadPipesData={jest.fn()}
272
+ isModal={true}
273
+ mode={VideoModalMode.MAXIMIZED}
274
+ {...props}
275
+ />
276
+ );
277
+
278
+ const pressHardwareBack = () => {
279
+ if (!mockBackHandlerRef.current) {
280
+ throw new Error("back handler was not registered via useBackHandler");
281
+ }
282
+
283
+ return mockBackHandlerRef.current();
284
+ };
285
+
286
+ describe("PlayerContainer hardware back handling", () => {
287
+ beforeEach(() => {
288
+ jest.clearAllMocks();
289
+ mockIsTV.mockReturnValue(false);
290
+ mockBackHandlerRef.current = null;
291
+ mockPlayer.getPluginConfiguration.mockReturnValue({});
292
+ mockPlayer.isPaused.mockReturnValue(false);
293
+ mockPlayer.isAd.mockReturnValue(false);
294
+
295
+ mockPlayer.getOverlayObservable.mockReturnValue({
296
+ getPlayNextEntry: () => null,
297
+ });
298
+
299
+ mockPlayerManager.getPluginConfiguration.mockReturnValue({});
300
+ mockPlayerManager.on.mockReturnThis();
301
+ mockPlayerManager.removeHandler.mockReturnThis();
302
+ });
303
+
304
+ it("minimises the video modal on back press when mini player is enabled", () => {
305
+ renderPlayerContainer();
306
+
307
+ const handled = pressHardwareBack();
308
+
309
+ expect(mockNavigator.minimiseVideoModal).toHaveBeenCalled();
310
+ expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled();
311
+ expect(handled).toBe(true);
312
+ });
313
+
314
+ it("closes the video modal on back press when mini player is disabled", () => {
315
+ mockPlayer.getPluginConfiguration.mockReturnValue({
316
+ disable_mini_player_when_inline: true,
317
+ });
318
+
319
+ renderPlayerContainer();
320
+
321
+ const handled = pressHardwareBack();
322
+
323
+ expect(mockNavigator.closeVideoModal).toHaveBeenCalled();
324
+ expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled();
325
+ expect(handled).toBe(true);
326
+ });
327
+
328
+ it("does not handle back press when the player is already minimised", () => {
329
+ renderPlayerContainer({ mode: VideoModalMode.MINIMIZED });
330
+
331
+ const handled = pressHardwareBack();
332
+
333
+ expect(handled).toBe(false);
334
+ expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled();
335
+ expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled();
336
+ });
337
+ });
@@ -2,7 +2,7 @@ import React from "react";
2
2
 
3
3
  type PreloaderWrapperProps = {
4
4
  showPreloader?: boolean;
5
- children?: React.ReactNode;
5
+ children: React.ReactElement | null;
6
6
  };
7
7
 
8
8
  export const PreloaderWrapper: React.FC<PreloaderWrapperProps> = ({
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
- import * as R from "ramda";
3
2
  import { FlatList, StyleSheet, View } from "react-native";
3
+ import { SafeAreaView, EdgeMode, Edge } from "react-native-safe-area-context";
4
4
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
5
5
  import { RiverItem } from "../RiverItem";
6
6
  import { RiverFooter } from "../RiverFooter";
@@ -9,7 +9,7 @@ import { useScreenConfiguration } from "../useScreenConfiguration";
9
9
  import { RefreshControl } from "../RefreshControl";
10
10
  import { ifEmptyUseFallback } from "@applicaster/zapp-react-native-utils/cellUtils";
11
11
  import {
12
- usePipesCacheReset,
12
+ useMarkPipesDataStale,
13
13
  useProfilerLogging,
14
14
  } from "@applicaster/zapp-react-native-utils/reactHooks";
15
15
  import { useLoadingState } from "./hooks/useLoadingState";
@@ -40,6 +40,12 @@ type Props = {
40
40
  riverId?: string;
41
41
  getStaticComponentFeed: any;
42
42
  stickyHeaderIndices?: number[];
43
+ safeArea?:
44
+ | {
45
+ edges: Record<Edge, EdgeMode>;
46
+ mode: "padding" | "margin" | undefined;
47
+ }
48
+ | undefined;
43
49
  };
44
50
 
45
51
  const styles = StyleSheet.create({
@@ -52,7 +58,7 @@ const getNearestValue = (value, optionA, optionB) =>
52
58
  Math.abs(value - optionA) < Math.abs(value - optionB) ? optionA : optionB;
53
59
 
54
60
  const scrollIndicatorInsets = { right: 1 };
55
- const keyExtractor = R.compose(String, R.prop("id"));
61
+ const keyExtractor = (item) => item.id.toString();
56
62
 
57
63
  function ComponentsMapComponent(props: Props) {
58
64
  const {
@@ -64,6 +70,7 @@ function ComponentsMapComponent(props: Props) {
64
70
  riverId,
65
71
  getStaticComponentFeed,
66
72
  stickyHeaderIndices,
73
+ safeArea,
67
74
  } = props;
68
75
 
69
76
  const flatListRef = React.useRef<FlatList | null>(null);
@@ -77,7 +84,7 @@ function ComponentsMapComponent(props: Props) {
77
84
 
78
85
  const riverComponents = React.useMemo(() => {
79
86
  if (feed?.entry?.length < components.length) {
80
- return R.slice(0, feed?.entry?.length, components);
87
+ return components.slice(0, feed?.entry?.length);
81
88
  }
82
89
 
83
90
  return components;
@@ -92,7 +99,7 @@ function ComponentsMapComponent(props: Props) {
92
99
  const { loadingState, onLoadFinished, onLoadFailed, shouldShowLoadingError } =
93
100
  useLoadingState(riverComponents.length, onLoadDone);
94
101
 
95
- const theme = useTheme();
102
+ const theme = useTheme<BaseThemePropertiesMobile>();
96
103
 
97
104
  const renderRiverItem = React.useCallback(
98
105
  ({ item, index }) => {
@@ -121,21 +128,13 @@ function ComponentsMapComponent(props: Props) {
121
128
 
122
129
  const screenStyle = React.useMemo(
123
130
  () => ({
124
- paddingTop: ifEmptyUseFallback(
125
- screenConfig.paddingTop,
126
- R.prop("screen_padding_top")(theme)
127
- ),
128
- paddingBottom: ifEmptyUseFallback(
129
- screenConfig.paddingBottom,
130
- R.prop("screen_padding_bottom")(theme)
131
- ),
132
131
  paddingLeft: ifEmptyUseFallback(
133
132
  screenConfig.paddingLeft,
134
- R.prop("screen_padding_left")(theme)
133
+ theme.screen_padding_left
135
134
  ),
136
135
  paddingRight: ifEmptyUseFallback(
137
136
  screenConfig.paddingRight,
138
- R.prop("screen_padding_right")(theme)
137
+ theme.screen_padding_right
139
138
  ),
140
139
  }),
141
140
  [riverId]
@@ -154,7 +153,7 @@ function ComponentsMapComponent(props: Props) {
154
153
  [flatListHeight]
155
154
  );
156
155
 
157
- usePipesCacheReset(riverId, riverComponents);
156
+ useMarkPipesDataStale(riverId, riverComponents);
158
157
 
159
158
  const refreshControl = React.useMemo(
160
159
  () => (pullToRefreshEnabled ? <RefreshControl /> : null),
@@ -266,51 +265,79 @@ function ComponentsMapComponent(props: Props) {
266
265
  numberOfComponents={riverComponents.length}
267
266
  >
268
267
  <ViewportTracker>
269
- <FlatList
270
- testID="components-map-flat-list"
271
- ref={(ref) => {
272
- flatListRef.current = ref;
273
- }}
274
- // Fix for WebView rerender crashes on Android API 28+
275
- // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
276
- overScrollMode={isAndroid ? "never" : "auto"}
277
- scrollIndicatorInsets={scrollIndicatorInsets}
278
- extraData={feed}
279
- stickyHeaderIndices={stickyHeaderIndices}
280
- removeClippedSubviews={isAndroid}
281
- onLayout={handleOnLayout}
282
- initialNumToRender={3}
283
- maxToRenderPerBatch={10}
284
- windowSize={12}
285
- keyExtractor={keyExtractor}
286
- renderItem={renderRiverItem}
287
- data={riverComponents}
288
- contentContainerStyle={contentContainerStyle}
289
- ListFooterComponent={
290
- <RiverFooter
291
- flatListHeight={flatListHeight}
292
- loadingState={loadingState}
293
- />
294
- }
295
- refreshControl={refreshControl}
296
- onScrollBeginDrag={onScrollBeginDrag}
297
- onScroll={onScroll}
298
- onMomentumScrollEnd={_onMomentumScrollEnd}
299
- onScrollEndDrag={_onScrollEndDrag}
300
- scrollEventThrottle={16}
301
- {...scrollViewExtraProps}
302
- onEndReached={
303
- /* When wrapped in a parent ScrollView (e.g. tabs),
268
+ <>
269
+ <FlatList
270
+ ListHeaderComponent={
271
+ <SafeAreaView
272
+ mode={safeArea?.mode ?? "margin"}
273
+ style={{
274
+ paddingTop: ifEmptyUseFallback(
275
+ screenConfig.paddingTop,
276
+ theme.screen_padding_top
277
+ ),
278
+ height: StyleSheet.hairlineWidth,
279
+ }}
280
+ edges={{ top: safeArea?.edges?.top ?? "off" }}
281
+ />
282
+ }
283
+ testID="components-map-flat-list"
284
+ ref={(ref) => {
285
+ flatListRef.current = ref;
286
+ }}
287
+ // Fix for WebView rerender crashes on Android API 28+
288
+ // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
289
+ overScrollMode={isAndroid ? "never" : "auto"}
290
+ scrollIndicatorInsets={scrollIndicatorInsets}
291
+ extraData={feed}
292
+ stickyHeaderIndices={stickyHeaderIndices}
293
+ removeClippedSubviews={isAndroid}
294
+ onLayout={handleOnLayout}
295
+ initialNumToRender={3}
296
+ maxToRenderPerBatch={10}
297
+ windowSize={12}
298
+ keyExtractor={keyExtractor}
299
+ renderItem={renderRiverItem}
300
+ data={riverComponents}
301
+ contentContainerStyle={contentContainerStyle}
302
+ ListFooterComponent={
303
+ <>
304
+ <RiverFooter
305
+ flatListHeight={flatListHeight}
306
+ loadingState={loadingState}
307
+ />
308
+ <SafeAreaView
309
+ mode={safeArea?.mode ?? "margin"}
310
+ style={{
311
+ paddingBottom: ifEmptyUseFallback(
312
+ screenConfig.paddingBottom,
313
+ theme.screen_padding_bottom
314
+ ),
315
+ height: StyleSheet.hairlineWidth,
316
+ }}
317
+ edges={{ bottom: safeArea?.edges?.bottom ?? "off" }}
318
+ />
319
+ </>
320
+ }
321
+ refreshControl={refreshControl}
322
+ onScrollBeginDrag={onScrollBeginDrag}
323
+ onScroll={onScroll}
324
+ onMomentumScrollEnd={_onMomentumScrollEnd}
325
+ onScrollEndDrag={_onScrollEndDrag}
326
+ scrollEventThrottle={16}
327
+ {...scrollViewExtraProps}
328
+ onEndReached={
329
+ /* When wrapped in a parent ScrollView (e.g. tabs),
304
330
  this FlatList doesn't scroll so onEndReached can fire repeatedly;
305
331
  skip it here and let the parent ScrollView emit scroll-end instead. */
306
- isScreenWrappedInContainer
307
- ? undefined
308
- : () => {
309
- if (!hasUserScrolledRef.current) return;
310
- emitScrollEndReached();
311
- }
312
- }
313
- />
332
+ isScreenWrappedInContainer
333
+ ? undefined
334
+ : () => {
335
+ if (!hasUserScrolledRef.current) return;
336
+ emitScrollEndReached();
337
+ }
338
+ }
339
+ />
340
+ </>
314
341
  </ViewportTracker>
315
342
  </ScreenLoadingMeasurements>
316
343
  </ComponentsMapRefContext.Provider>
@@ -320,6 +347,5 @@ function ComponentsMapComponent(props: Props) {
320
347
  }
321
348
 
322
349
  export const ComponentsMap = React.memo(
323
- withComponentsMapProvider(ComponentsMapComponent),
324
- R.equals
350
+ withComponentsMapProvider(ComponentsMapComponent)
325
351
  );
@@ -20,6 +20,8 @@ type Props = ZappScreenProps & {
20
20
  setIsScreenWrappedInContainer: (isInsideContainer: boolean) => void;
21
21
  screenContext: ZappRiver;
22
22
  river: ZappRiver;
23
+ /** Result channel for screens presented as a modal; forwarded to the resolved screen. */
24
+ resultCallback?: hookCallback;
23
25
  };
24
26
 
25
27
  export class RiverComponent extends React.Component<Props> {
@@ -64,6 +66,7 @@ export class RiverComponent extends React.Component<Props> {
64
66
  isInsideContainer,
65
67
  groupId,
66
68
  scrollViewExtraProps,
69
+ resultCallback,
67
70
  } = this.props;
68
71
 
69
72
  const { id, type } = river;
@@ -77,6 +80,7 @@ export class RiverComponent extends React.Component<Props> {
77
80
  })}
78
81
  screenId={id}
79
82
  screenType={type}
83
+ resultCallback={resultCallback}
80
84
  />
81
85
  );
82
86
  }