@applicaster/zapp-react-native-ui-components 16.0.0-alpha.6861239588 → 16.0.0-alpha.6918106967

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 (83) 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/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +15 -0
  7. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  8. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +64 -0
  9. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  10. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +18 -0
  11. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +1 -0
  12. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  13. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  14. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  15. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  16. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  17. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  18. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +54 -0
  19. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +21 -0
  20. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  21. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +58 -0
  22. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -18
  23. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +32 -27
  24. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +25 -73
  25. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +0 -18
  26. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +166 -84
  27. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
  28. package/Components/ModalComponent/AudioPlayer/Components/index.ts +3 -1
  29. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +230 -0
  30. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +332 -0
  31. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  32. package/Components/ModalComponent/BottomSheetModalContent.tsx +385 -60
  33. package/Components/ModalComponent/Button/index.tsx +5 -3
  34. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  35. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  36. package/Components/ModalComponent/Header/index.tsx +20 -7
  37. package/Components/ModalComponent/Header/utils.ts +1 -1
  38. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  39. package/Components/ModalComponent/SortableList.tsx +142 -0
  40. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  41. package/Components/ModalComponent/TextInputContent.tsx +100 -0
  42. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  43. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
  44. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  45. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
  46. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  47. package/Components/ModalComponent/index.tsx +11 -0
  48. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
  49. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  50. package/Components/ModalComponent/presets/index.ts +27 -0
  51. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  52. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  53. package/Components/ModalComponent/presets/types.ts +35 -0
  54. package/Components/ModalComponent/showTextInput.ts +43 -0
  55. package/Components/ModalComponent/utils.ts +82 -21
  56. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  57. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  58. package/Components/OfflineHandler/hooks.ts +111 -0
  59. package/Components/OfflineHandler/index.tsx +51 -73
  60. package/Components/OfflineHandler/utils/index.ts +1 -13
  61. package/Components/PlayerContainer/PlayerContainer.tsx +11 -7
  62. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  63. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +66 -13
  64. package/Components/PreloaderWrapper/index.tsx +1 -1
  65. package/Components/River/ComponentsMap/ComponentsMap.tsx +79 -51
  66. package/Components/River/River.tsx +4 -0
  67. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  68. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  69. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  70. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  71. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  72. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  73. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +20 -3
  74. package/Components/TopCutoffOverlay/index.tsx +2 -2
  75. package/Components/VideoLive/LiveImageManager.ts +1 -1
  76. package/Hooks/useEntryActionsExpander.ts +63 -0
  77. package/package.json +6 -5
  78. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  79. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  80. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  81. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  82. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  83. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -9,6 +9,7 @@ import {
9
9
  platformSelect,
10
10
  isAndroidTVPlatform,
11
11
  isTvOSPlatform,
12
+ isApplePlatform,
12
13
  } from "@applicaster/zapp-react-native-utils/reactUtils";
13
14
  import {
14
15
  isAudioItem,
@@ -60,6 +61,7 @@ import {
60
61
  PlayerNativeSendCommand,
61
62
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
62
63
  import { useAppData } from "@applicaster/zapp-react-native-redux";
64
+ import { PLAYER_EVENTS } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
63
65
 
64
66
  type Props = {
65
67
  Player: React.ComponentType<any>;
@@ -188,12 +190,10 @@ const renderApplePlayer = ({
188
190
  return null;
189
191
  }
190
192
 
191
- const { title, summary } = item || {};
192
-
193
+ // Title/Subtitle are not resolved here: `ProgramInfo` reads them from the
194
+ // player manager's content channel via `usePlayerContent`.
193
195
  return (
194
196
  <ProgramInfo
195
- title={title}
196
- subtitle={summary}
197
197
  plugin_configuration={pluginConfiguration}
198
198
  videoStyle={videoStyle}
199
199
  />
@@ -282,12 +282,16 @@ const PlayerContainerComponent = (props: Props) => {
282
282
  }
283
283
 
284
284
  log_debug(
285
- `onPlayNextPerformNextVideoPlay: delay completed, replaceTop to item: ${playNextOverlayState.entry.title}, id: ${playNextOverlayState.entry.id}`
285
+ `onPlayNextPerformNextVideoPlay: delay completed, replaceTop to item: ${playNextOverlayState.entry.title}, id: ${playNextOverlayState.entry.id}, outgoing item id: ${item?.id}`
286
286
  );
287
287
 
288
+ playNextOverlayState.hidePlayNext();
289
+
290
+ playerEvent(PLAYER_EVENTS.PlayNextTriggered, { id: item?.id });
291
+
288
292
  setIsLoadingNextVideo(true);
289
293
  playEntry(playNextOverlayState.entry);
290
- }, [playNextOverlayState]);
294
+ }, [playNextOverlayState, item?.id]);
291
295
 
292
296
  const playNextData: Nullable<PlayNextData> = React.useMemo(() => {
293
297
  if (!playNextOverlayState) {
@@ -495,7 +499,7 @@ const PlayerContainerComponent = (props: Props) => {
495
499
  const isAudioContent = isAudioItem(item);
496
500
 
497
501
  useEffect(() => {
498
- if (!isAudioContent) {
502
+ if (!isAudioContent && isApplePlatform()) {
499
503
  sendQuickBrickEvent(
500
504
  QUICK_BRICK_EVENTS.IDLE_TIMER_DISABLED as QuickBrickEvent,
501
505
  {
@@ -7,12 +7,11 @@ import * as React from "react";
7
7
  import { Text, TextStyle, View, ViewStyle } from "react-native";
8
8
  import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
9
9
 
10
+ import { usePlayerContent } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerContent";
10
11
  import * as programStyles from "./styles";
11
12
 
12
13
  type Props = {
13
14
  logo?: string;
14
- title: string;
15
- subtitle: string;
16
15
  isAudioContent: boolean;
17
16
  styles: {};
18
17
  plugin_configuration: {
@@ -28,13 +27,13 @@ const defaultColor = "#EFEFEF";
28
27
 
29
28
  function ProgramInfoComponent({
30
29
  logo,
31
- title = "",
32
- subtitle = "",
33
30
  isAudioContent,
34
31
  styles,
35
32
  plugin_configuration = {},
36
33
  videoStyle = {},
37
34
  }: Props) {
35
+ const { title = "", summary: subtitle = "" } = usePlayerContent();
36
+
38
37
  const platformKey = styleKeys.style_namespace;
39
38
 
40
39
  const colorTitle =
@@ -17,16 +17,37 @@ jest.mock(
17
17
  );
18
18
 
19
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).
20
25
  isTV: jest.fn(() => false),
21
26
  isAndroidTVPlatform: jest.fn(() => false),
22
- isTvOSPlatform: 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),
23
34
  platformSelect: jest.fn(({ native }) => native),
24
35
  }));
25
36
 
26
- jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () => ({
27
- isAudioItem: jest.fn(() => false),
28
- isInlineTV: jest.fn(() => false),
29
- }));
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
+ });
30
51
 
31
52
  jest.mock(
32
53
  "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent",
@@ -75,6 +96,11 @@ jest.mock("@applicaster/zapp-react-native-bridge/QuickBrick", () => ({
75
96
  sendQuickBrickEvent: jest.fn(),
76
97
  }));
77
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`.
78
104
  jest.mock("../ProgramInfo", () => ({
79
105
  ProgramInfo: () => null,
80
106
  }));
@@ -142,14 +168,33 @@ jest.mock(
142
168
  jest.mock("../PlayerContainerContext", () => {
143
169
  const ReactActual = require("react");
144
170
 
171
+ const PlayerContainerContext = ReactActual.createContext({
172
+ isLanguageOverlayVisible: false,
173
+ bottomFocusableId: "bottom",
174
+ showComponentsContainer: false,
175
+ refs: null,
176
+ });
177
+
145
178
  return {
146
- PlayerContainerContext: ReactActual.createContext({
147
- isLanguageOverlayVisible: false,
148
- bottomFocusableId: "bottom",
149
- showComponentsContainer: false,
150
- refs: null,
151
- }),
152
- PlayerContainerContextProvider: ({ children }) => children,
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
+ ),
153
198
  };
154
199
  });
155
200
 
@@ -200,7 +245,14 @@ const { __player: mockPlayer } = jest.requireMock(
200
245
  "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer"
201
246
  );
202
247
 
203
- const Player = React.forwardRef(() => null);
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);
204
256
 
205
257
  const item = {
206
258
  id: "entry-1",
@@ -234,6 +286,7 @@ const pressHardwareBack = () => {
234
286
  describe("PlayerContainer hardware back handling", () => {
235
287
  beforeEach(() => {
236
288
  jest.clearAllMocks();
289
+ mockIsTV.mockReturnValue(false);
237
290
  mockBackHandlerRef.current = null;
238
291
  mockPlayer.getPluginConfiguration.mockReturnValue({});
239
292
  mockPlayer.isPaused.mockReturnValue(false);
@@ -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,7 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
3
  import { FlatList, StyleSheet, View } from "react-native";
4
+ import { SafeAreaView, EdgeMode, Edge } from "react-native-safe-area-context";
4
5
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
5
6
  import { RiverItem } from "../RiverItem";
6
7
  import { RiverFooter } from "../RiverFooter";
@@ -40,6 +41,12 @@ type Props = {
40
41
  riverId?: string;
41
42
  getStaticComponentFeed: any;
42
43
  stickyHeaderIndices?: number[];
44
+ safeArea?:
45
+ | {
46
+ edges: Record<Edge, EdgeMode>;
47
+ mode: "padding" | "margin" | undefined;
48
+ }
49
+ | undefined;
43
50
  };
44
51
 
45
52
  const styles = StyleSheet.create({
@@ -64,6 +71,7 @@ function ComponentsMapComponent(props: Props) {
64
71
  riverId,
65
72
  getStaticComponentFeed,
66
73
  stickyHeaderIndices,
74
+ safeArea,
67
75
  } = props;
68
76
 
69
77
  const flatListRef = React.useRef<FlatList | null>(null);
@@ -121,14 +129,6 @@ function ComponentsMapComponent(props: Props) {
121
129
 
122
130
  const screenStyle = React.useMemo(
123
131
  () => ({
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
132
  paddingLeft: ifEmptyUseFallback(
133
133
  screenConfig.paddingLeft,
134
134
  R.prop("screen_padding_left")(theme)
@@ -266,51 +266,79 @@ function ComponentsMapComponent(props: Props) {
266
266
  numberOfComponents={riverComponents.length}
267
267
  >
268
268
  <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),
269
+ <>
270
+ <FlatList
271
+ ListHeaderComponent={
272
+ <SafeAreaView
273
+ mode="margin"
274
+ style={{
275
+ paddingTop: ifEmptyUseFallback(
276
+ screenConfig.paddingTop,
277
+ R.prop("screen_padding_top")(theme)
278
+ ),
279
+ height: StyleSheet.hairlineWidth,
280
+ }}
281
+ edges={{ top: safeArea?.edges?.top ?? "off" }}
282
+ />
283
+ }
284
+ testID="components-map-flat-list"
285
+ ref={(ref) => {
286
+ flatListRef.current = ref;
287
+ }}
288
+ // Fix for WebView rerender crashes on Android API 28+
289
+ // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
290
+ overScrollMode={isAndroid ? "never" : "auto"}
291
+ scrollIndicatorInsets={scrollIndicatorInsets}
292
+ extraData={feed}
293
+ stickyHeaderIndices={stickyHeaderIndices}
294
+ removeClippedSubviews={isAndroid}
295
+ onLayout={handleOnLayout}
296
+ initialNumToRender={3}
297
+ maxToRenderPerBatch={10}
298
+ windowSize={12}
299
+ keyExtractor={keyExtractor}
300
+ renderItem={renderRiverItem}
301
+ data={riverComponents}
302
+ contentContainerStyle={contentContainerStyle}
303
+ ListFooterComponent={
304
+ <>
305
+ <RiverFooter
306
+ flatListHeight={flatListHeight}
307
+ loadingState={loadingState}
308
+ />
309
+ <SafeAreaView
310
+ mode="margin"
311
+ style={{
312
+ paddingBottom: ifEmptyUseFallback(
313
+ screenConfig.paddingBottom,
314
+ R.prop("screen_padding_bottom")(theme)
315
+ ),
316
+ height: StyleSheet.hairlineWidth,
317
+ }}
318
+ edges={{ bottom: safeArea?.edges?.bottom ?? "off" }}
319
+ />
320
+ </>
321
+ }
322
+ refreshControl={refreshControl}
323
+ onScrollBeginDrag={onScrollBeginDrag}
324
+ onScroll={onScroll}
325
+ onMomentumScrollEnd={_onMomentumScrollEnd}
326
+ onScrollEndDrag={_onScrollEndDrag}
327
+ scrollEventThrottle={16}
328
+ {...scrollViewExtraProps}
329
+ onEndReached={
330
+ /* When wrapped in a parent ScrollView (e.g. tabs),
304
331
  this FlatList doesn't scroll so onEndReached can fire repeatedly;
305
332
  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
- />
333
+ isScreenWrappedInContainer
334
+ ? undefined
335
+ : () => {
336
+ if (!hasUserScrolledRef.current) return;
337
+ emitScrollEndReached();
338
+ }
339
+ }
340
+ />
341
+ </>
314
342
  </ViewportTracker>
315
343
  </ScreenLoadingMeasurements>
316
344
  </ComponentsMapRefContext.Provider>
@@ -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
  }
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+
4
+ import { RiverComponent } from "../River";
5
+
6
+ const mockResolverProps: any[] = [];
7
+
8
+ jest.mock("../../ScreenResolver", () => ({
9
+ ScreenResolver: (props: any) => {
10
+ mockResolverProps.push(props);
11
+
12
+ return null;
13
+ },
14
+ }));
15
+
16
+ const river = { id: "river-parent-lock", type: "parent_lock" } as any;
17
+
18
+ const baseProps = {
19
+ river,
20
+ screenData: {},
21
+ groupId: "group-1",
22
+ scrollViewExtraProps: {},
23
+ setIsScreenWrappedInContainer: jest.fn(),
24
+ screenContext: { navBar: { setTitle: jest.fn(), setSummary: jest.fn() } },
25
+ } as any;
26
+
27
+ const resolved = () => mockResolverProps[mockResolverProps.length - 1];
28
+
29
+ describe("RiverComponent", () => {
30
+ beforeEach(() => {
31
+ mockResolverProps.length = 0;
32
+ });
33
+
34
+ it("forwards resultCallback to the screen it resolves", () => {
35
+ const resultCallback = jest.fn();
36
+
37
+ render(<RiverComponent {...baseProps} resultCallback={resultCallback} />);
38
+
39
+ expect(resolved().resultCallback).toBe(resultCallback);
40
+ });
41
+
42
+ it("resolves the screen without a resultCallback when none was given", () => {
43
+ render(<RiverComponent {...baseProps} />);
44
+
45
+ expect(resolved().resultCallback).toBeUndefined();
46
+ });
47
+ });
@@ -8,9 +8,11 @@ import { componentsLogger } from "../../Helpers/logger";
8
8
  const logger = componentsLogger.addSubsystem("ScreenFeedLoader");
9
9
 
10
10
  /** Loads and provides `feedData` and store to */
11
- export const ScreenFeedLoader: React.FC<
12
- React.PropsWithChildren<{ id: string; feedData: any }>
13
- > = ({ id, feedData, children }) => {
11
+ export const ScreenFeedLoader: React.FC<{
12
+ id: string;
13
+ feedData: any;
14
+ children: React.ReactElement | null;
15
+ }> = ({ id, feedData, children }) => {
14
16
  const { source: feedUrl, mapping } = feedData;
15
17
 
16
18
  const { data, loading, error } = useFeedLoader({
@@ -8,7 +8,7 @@ export const ScreenResolverFeedProvider = ({
8
8
  useFeedData,
9
9
  }: {
10
10
  id: string;
11
- children: React.ReactNode;
11
+ children: React.ReactElement | null;
12
12
  useFeedData: (id: string) => Option<ZappDataSource>;
13
13
  }) => {
14
14
  const feedData = useFeedData(id);
@@ -42,9 +42,25 @@ export class ScreenRevealManager {
42
42
  private subject$ = new Subject<void>();
43
43
  private subscription: Subscription;
44
44
 
45
- constructor(componentsToRender: ZappUIComponent[], callback: Callback) {
45
+ /**
46
+ * @param initialNumberToLoad how many components to wait for before revealing
47
+ * the screen. Staggering exists so a screen full of network-backed components
48
+ * does not load them all at once. A caller that has nothing to stagger - see
49
+ * `disableIncrementalLoading` on the HOC - passes the full count here so the
50
+ * reveal waits for exactly what gets rendered. Omitted, the count is derived
51
+ * from the components themselves.
52
+ */
53
+ constructor(
54
+ componentsToRender: ZappUIComponent[],
55
+ callback: Callback,
56
+ initialNumberToLoad?: number
57
+ ) {
46
58
  this.numberOfComponentsWaitToLoadBeforePresent =
47
- getNumberOfComponentsWaitToLoadBeforePresent(componentsToRender);
59
+ initialNumberToLoad == null
60
+ ? getNumberOfComponentsWaitToLoadBeforePresent(componentsToRender)
61
+ : // Clamped like the computed path: waiting for components that do not
62
+ // exist would hold the screen behind its overlay until the timeout.
63
+ Math.min(initialNumberToLoad, componentsToRender.length);
48
64
 
49
65
  this.renderingState = makeListOf<ComponentLoadingState>(
50
66
  COMPONENT_LOADING_STATE.UNKNOWN,
@@ -22,6 +22,42 @@ import { makeListOf } from "@applicaster/zapp-react-native-utils/arrayUtils";
22
22
  import { isFirstComponentGallery } from "@applicaster/zapp-react-native-utils/componentsUtils";
23
23
  import { withTimeout$ } from "@applicaster/zapp-react-native-utils/idleUtils";
24
24
 
25
+ describe("ScreenRevealManager explicit component count", () => {
26
+ beforeEach(() => {
27
+ (isFirstComponentGallery as jest.Mock).mockReturnValue(false);
28
+
29
+ (makeListOf as jest.Mock).mockImplementation((value, length) =>
30
+ Array(length).fill(value)
31
+ );
32
+
33
+ (withTimeout$ as jest.Mock).mockReturnValue(new Subject());
34
+ });
35
+
36
+ it("waits for the number of components it is given, not the computed default", () => {
37
+ const components = Array(6).fill({}) as any;
38
+
39
+ const manager = new ScreenRevealManager(components, jest.fn(), 6);
40
+
41
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(6);
42
+ });
43
+
44
+ it("never waits for more components than it was handed", () => {
45
+ const components = Array(3).fill({}) as any;
46
+
47
+ const manager = new ScreenRevealManager(components, jest.fn(), 10);
48
+
49
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(3);
50
+ });
51
+
52
+ it("falls back to the computed default when given no explicit count", () => {
53
+ const components = Array(6).fill({}) as any;
54
+
55
+ const manager = new ScreenRevealManager(components, jest.fn());
56
+
57
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(3);
58
+ });
59
+ });
60
+
25
61
  describe("ScreenRevealManager", () => {
26
62
  let mockCallback: jest.Mock;
27
63
  let timeout$: Subject<void>;
@@ -9,6 +9,12 @@ import {
9
9
  TIMEOUT,
10
10
  } from "../withScreenRevealManager";
11
11
 
12
+ jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
13
+ useTheme: () => ({ app_background_color: "#000000" }),
14
+ }));
15
+
16
+ jest.mock("../Overlay", () => ({ Overlay: () => null }));
17
+
12
18
  // jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");
13
19
 
14
20
  const MockComponent = ({
@@ -37,6 +43,56 @@ const MockComponent = ({
37
43
 
38
44
  const WrappedComponent = withScreenRevealManager(MockComponent);
39
45
 
46
+ describe("withScreenRevealManager disableIncrementalLoading", () => {
47
+ const components = (count: number) =>
48
+ Array(count).fill({ component_type: "grid-qb" });
49
+
50
+ beforeEach(() => {
51
+ jest.clearAllMocks();
52
+ });
53
+
54
+ it("asks for every component at once when incremental loading is disabled", () => {
55
+ render(
56
+ <WrappedComponent
57
+ componentsToRender={components(6)}
58
+ disableIncrementalLoading
59
+ />
60
+ );
61
+
62
+ expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
63
+ 6
64
+ );
65
+ });
66
+
67
+ it("still tracks the component count after it changes", () => {
68
+ const { rerender } = render(
69
+ <WrappedComponent
70
+ componentsToRender={components(6)}
71
+ disableIncrementalLoading
72
+ />
73
+ );
74
+
75
+ rerender(
76
+ <WrappedComponent
77
+ componentsToRender={components(9)}
78
+ disableIncrementalLoading
79
+ />
80
+ );
81
+
82
+ expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
83
+ 9
84
+ );
85
+ });
86
+
87
+ it("loads incrementally when the flag is not set", () => {
88
+ render(<WrappedComponent componentsToRender={components(6)} />);
89
+
90
+ expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
91
+ 3
92
+ );
93
+ });
94
+ });
95
+
40
96
  describe.skip("withScreenRevealManager", () => {
41
97
  beforeEach(() => {
42
98
  jest.clearAllMocks();
@@ -21,11 +21,23 @@ export const SHOWN = 1; // opacity = 1
21
21
  type Props = {
22
22
  componentsToRender: ZappUIComponent[];
23
23
  backgroundColor?: string;
24
+ /**
25
+ * Renders every component at once instead of revealing them a few at a time.
26
+ *
27
+ * For a screen that builds its own components and holds all of them already -
28
+ * a form, say - staggering buys nothing and costs: each rebuild of the list
29
+ * tears it back down to the first few and lets the rest crawl in again.
30
+ *
31
+ * This is a flag rather than a count on purpose. A count would have to be
32
+ * re-read whenever the number of components changes, while "do not stagger"
33
+ * stays true whatever the screen ends up holding.
34
+ */
35
+ disableIncrementalLoading?: boolean;
24
36
  };
25
37
 
26
38
  export const withScreenRevealManager = (Component) => {
27
39
  return function WithScreenRevealManager(props: Props) {
28
- const { componentsToRender } = props;
40
+ const { componentsToRender, disableIncrementalLoading } = props;
29
41
 
30
42
  const [isContentReadyToBeShown, setIsContentReadyToBeShown] =
31
43
  React.useState(false);
@@ -42,7 +54,8 @@ export const withScreenRevealManager = (Component) => {
42
54
  () =>
43
55
  new ScreenRevealManager(
44
56
  componentsToRender,
45
- handleSetIsContentReadyToBeShown
57
+ handleSetIsContentReadyToBeShown,
58
+ disableIncrementalLoading ? componentsToRender.length : undefined
46
59
  )
47
60
  );
48
61
 
@@ -80,7 +93,11 @@ export const withScreenRevealManager = (Component) => {
80
93
  <Component
81
94
  {...props}
82
95
  initialNumberToLoad={
83
- managerRef.current.numberOfComponentsWaitToLoadBeforePresent
96
+ // Recomputed on every render, so the count follows the components
97
+ // even though the manager settled its own expectation at mount.
98
+ disableIncrementalLoading
99
+ ? componentsToRender.length
100
+ : managerRef.current.numberOfComponentsWaitToLoadBeforePresent
84
101
  }
85
102
  onLoadFinishedFromScreenRevealManager={
86
103
  managerRef.current.onLoadFinished