@applicaster/zapp-react-native-ui-components 15.0.0-rc.99 → 16.0.0-rc.1

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 (138) hide show
  1. package/Components/Cell/TvOSCellComponent.tsx +1 -3
  2. package/Components/GeneralContentScreen/GeneralContentScreen.tsx +39 -28
  3. package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
  4. package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
  5. package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
  6. package/Components/HandlePlayable/HandlePlayable.tsx +16 -29
  7. package/Components/HandlePlayable/utils.ts +31 -0
  8. package/Components/HookRenderer/HookRenderer.tsx +40 -10
  9. package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
  10. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  11. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  12. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  13. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +5 -0
  14. package/Components/MasterCell/CONFIG_BUILDER_TO_REACT_COMPONENT.md +144 -0
  15. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  16. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  17. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  18. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  19. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +165 -0
  20. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +405 -0
  21. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/index.ts +1 -0
  22. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  23. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  24. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  25. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  27. package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
  28. package/Components/MasterCell/DefaultComponents/ButtonContainerView/components/HorizontalSeparator.tsx +8 -0
  29. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +15 -0
  30. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tv.android.tsx +58 -0
  31. package/Components/MasterCell/DefaultComponents/{tv/ButtonContainerView/index.tsx → ButtonContainerView/index.tv.tsx} +3 -11
  32. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.web.ts +1 -0
  33. package/Components/MasterCell/DefaultComponents/ButtonContainerView/types.ts +40 -0
  34. package/Components/MasterCell/DefaultComponents/DataProvider/index.tsx +163 -0
  35. package/Components/MasterCell/DefaultComponents/FocusableView/index.android.tsx +2 -23
  36. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -22
  37. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +3 -1
  38. package/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
  39. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +136 -0
  40. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +33 -16
  41. package/Components/MasterCell/DefaultComponents/PressableView.tsx +34 -0
  42. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +11 -0
  43. package/Components/MasterCell/DefaultComponents/Text/index.tsx +2 -6
  44. package/Components/MasterCell/DefaultComponents/__tests__/DataProvider.test.tsx +141 -0
  45. package/Components/MasterCell/DefaultComponents/index.ts +9 -3
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +135 -0
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +33 -0
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +22 -0
  49. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +125 -0
  50. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  51. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  52. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +37 -0
  53. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +393 -0
  54. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +141 -0
  55. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +343 -0
  56. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  57. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +122 -0
  58. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  59. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +238 -0
  60. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Asset.ts +4 -18
  61. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Button.ts +24 -73
  62. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TextLabelsContainer.ts +37 -18
  63. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TvActionButton.tsx +27 -0
  64. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +89 -0
  65. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/renderedTree.test.tsx +231 -0
  66. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +47 -52
  67. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +35 -171
  68. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +98 -145
  69. package/Components/MasterCell/MappingFunctions/index.js +3 -2
  70. package/Components/MasterCell/README.md +4 -0
  71. package/Components/MasterCell/__tests__/__snapshots__/dataAdapter.test.js.snap +24 -0
  72. package/Components/MasterCell/__tests__/configInflater.test.js +1 -0
  73. package/Components/MasterCell/__tests__/elementMapper.test.js +46 -0
  74. package/Components/MasterCell/dataAdapter.ts +4 -1
  75. package/Components/MasterCell/elementMapper.tsx +52 -7
  76. package/Components/MasterCell/utils/__tests__/cloneChildrenWithIds.test.tsx +43 -0
  77. package/Components/MasterCell/utils/__tests__/useFilterChildren.test.tsx +80 -0
  78. package/Components/MasterCell/utils/index.ts +85 -15
  79. package/Components/Navigator/StackNavigator.tsx +6 -0
  80. package/Components/PlayerContainer/PlayerContainer.tsx +2 -19
  81. package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
  82. package/Components/PreloaderWrapper/index.tsx +15 -0
  83. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -16
  84. package/Components/River/RefreshControl.tsx +19 -82
  85. package/Components/River/River.tsx +9 -82
  86. package/Components/River/RiverItem.tsx +26 -20
  87. package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
  88. package/Components/River/hooks/index.ts +1 -0
  89. package/Components/River/hooks/usePullToRefresh.ts +51 -0
  90. package/Components/Screen/__tests__/Screen.test.tsx +1 -0
  91. package/Components/Screen/hooks.ts +73 -3
  92. package/Components/Screen/index.tsx +7 -1
  93. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
  94. package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
  95. package/Components/ScreenFeedLoader/index.ts +1 -0
  96. package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
  97. package/Components/ScreenResolver/hooks/index.ts +3 -0
  98. package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
  99. package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
  100. package/Components/ScreenResolver/index.tsx +15 -117
  101. package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
  102. package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
  103. package/Components/ScreenResolver/utils/index.ts +1 -0
  104. package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
  105. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
  106. package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
  107. package/Components/ScreenResolverFeedProvider/index.ts +1 -0
  108. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +4 -1
  109. package/Components/TopCutoffOverlay/__tests__/TopCutoffOverlay.test.tsx +201 -0
  110. package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
  111. package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
  112. package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
  113. package/Components/TopCutoffOverlay/index.tsx +55 -0
  114. package/Components/Transitioner/Scene.tsx +9 -15
  115. package/Components/VideoLive/LiveImageManager.ts +199 -54
  116. package/Components/VideoLive/PlayerLiveImageComponent.tsx +31 -33
  117. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
  118. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
  119. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  120. package/Components/ZappUIComponent/index.tsx +12 -6
  121. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  122. package/Components/index.js +1 -1
  123. package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
  124. package/Contexts/ScreenContext/index.tsx +46 -1
  125. package/Contexts/ZappPipesContext/ZappPipesContextFactory.tsx +18 -7
  126. package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -7
  127. package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +212 -5
  128. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
  129. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
  130. package/package.json +5 -5
  131. package/Components/MasterCell/DefaultComponents/Text/utils/__tests__/withAdjustedLineHeight.test.ts +0 -46
  132. package/Components/MasterCell/DefaultComponents/Text/utils/index.ts +0 -21
  133. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +0 -135
  134. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/types.ts +0 -25
  135. package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
  136. package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
  137. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +0 -101
  138. /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
@@ -2,7 +2,6 @@ import * as React from "react";
2
2
  import * as R from "ramda";
3
3
  import { GeneralContentScreen } from "@applicaster/zapp-react-native-ui-components/Components/GeneralContentScreen";
4
4
 
5
- import { FeedLoader } from "../FeedLoader";
6
5
  import { ScreenResolver } from "../ScreenResolver";
7
6
 
8
7
  type Props = ZappScreenProps & {
@@ -23,24 +22,13 @@ type Props = ZappScreenProps & {
23
22
  river: ZappRiver;
24
23
  };
25
24
 
26
- type State = {
27
- refreshing: boolean;
28
- };
29
-
30
- export class RiverComponent extends React.Component<Props, State> {
25
+ export class RiverComponent extends React.Component<Props> {
31
26
  private currentScreenTitle: string;
32
27
  private currentScreenSummary: string;
33
28
  constructor(props: Props) {
34
29
  super(props);
35
30
 
36
- this.state = {
37
- refreshing: false,
38
- };
39
-
40
31
  this.applyContexts();
41
-
42
- this.pullToRefreshPipesV1RefreshingStateUpdater =
43
- this.pullToRefreshPipesV1RefreshingStateUpdater.bind(this);
44
32
  }
45
33
 
46
34
  applyContexts() {
@@ -69,20 +57,9 @@ export class RiverComponent extends React.Component<Props, State> {
69
57
  }
70
58
  }
71
59
 
72
- usesPipesV1Layout() {
73
- return this.props?.appData?.layoutVersion === "v1";
74
- }
75
-
76
- // Method added to keep pipes v1 logic up to date with the pullToRefresh state.
77
- // TODO: Remove when pipes v1 is deprecated.
78
- pullToRefreshPipesV1RefreshingStateUpdater(refreshing) {
79
- this.setState({ refreshing });
80
- }
81
-
82
60
  render() {
83
61
  const {
84
62
  river,
85
- feedUrl,
86
63
  screenData,
87
64
  isInsideContainer,
88
65
  groupId,
@@ -91,22 +68,10 @@ export class RiverComponent extends React.Component<Props, State> {
91
68
 
92
69
  const { id, type } = river;
93
70
 
94
- const connectedFeedURL = R.path(["content", "src"], screenData);
95
- const _feedUrl = feedUrl || connectedFeedURL;
96
-
97
71
  if (type !== "general_content") {
98
- let riverWithConnectedDatasource;
99
-
100
- if (_feedUrl && this.usesPipesV1Layout()) {
101
- riverWithConnectedDatasource = {
102
- ...river,
103
- data: R.merge(river.data || {}, { source: _feedUrl }),
104
- };
105
- }
106
-
107
72
  return (
108
73
  <ScreenResolver
109
- screenData={R.mergeLeft(riverWithConnectedDatasource || river, {
74
+ screenData={R.mergeLeft(river, {
110
75
  groupId,
111
76
  ...screenData,
112
77
  })}
@@ -116,53 +81,15 @@ export class RiverComponent extends React.Component<Props, State> {
116
81
  );
117
82
  }
118
83
 
119
- if (!_feedUrl || !this.usesPipesV1Layout()) {
120
- this.currentScreenTitle = (screenData && screenData.title) || null;
121
-
122
- return (
123
- <GeneralContentScreen
124
- screenId={id}
125
- isScreenWrappedInContainer={isInsideContainer}
126
- groupId={groupId}
127
- scrollViewExtraProps={scrollViewExtraProps}
128
- />
129
- );
130
- }
84
+ this.currentScreenTitle = (screenData && screenData.title) || null;
131
85
 
132
86
  return (
133
- <FeedLoader
134
- feedUrl={_feedUrl}
135
- refreshing={this.state.refreshing}
136
- refreshCallback={() =>
137
- this.pullToRefreshPipesV1RefreshingStateUpdater(false)
138
- }
139
- >
140
- {(feed) => {
141
- if (!feed) {
142
- return null;
143
- }
144
-
145
- this.currentScreenSummary = (feed && feed.summary) || null;
146
-
147
- this.currentScreenTitle =
148
- (feed && feed.title) || (screenData && screenData.title) || null;
149
-
150
- return (
151
- <GeneralContentScreen
152
- screenId={id}
153
- groupId={groupId}
154
- feed={feed}
155
- isScreenWrappedInContainer={isInsideContainer}
156
- scrollViewExtraProps={scrollViewExtraProps}
157
- componentsMapExtraProps={{
158
- pullToRefreshPipesV1RefreshingStateUpdater:
159
- this.pullToRefreshPipesV1RefreshingStateUpdater,
160
- refreshingPipesV1: this.state.refreshing,
161
- }}
162
- />
163
- );
164
- }}
165
- </FeedLoader>
87
+ <GeneralContentScreen
88
+ screenId={id}
89
+ isScreenWrappedInContainer={isInsideContainer}
90
+ groupId={groupId}
91
+ scrollViewExtraProps={scrollViewExtraProps}
92
+ />
166
93
  );
167
94
  }
168
95
  }
@@ -14,6 +14,7 @@ import { tvPluginsWithCellRenderer } from "../../const";
14
14
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
15
15
  import type { BehaviorSubject } from "rxjs";
16
16
  import { useCallbackActions } from "@applicaster/zapp-react-native-utils/zappFrameworkUtils/HookCallback/useCallbackActions";
17
+ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
17
18
 
18
19
  export type RiverItemType = {
19
20
  item: ZappUIComponent;
@@ -112,33 +113,38 @@ function RiverItemComponent(props: RiverItemType) {
112
113
  CellRenderer = undefined;
113
114
  }
114
115
 
115
- React.useEffect(() => {
116
- riverLogger.log({
117
- message: "mounting component",
118
- data: { item, feedUrl, Component, CellRenderer },
119
- jsOnly: true,
120
- });
116
+ const isComponentMissing = isNilOrEmpty(Component);
121
117
 
122
- if (!CellRenderer && !isGroup(item)) {
118
+ /**
119
+ * TODO: Move this plugin existence check further up the stack (before ComponentsMap).
120
+ * Filtering items at the list-rendering or data-processing level would prevent
121
+ * mounting RiverItem entirely for missing components.
122
+ */
123
+ React.useEffect(() => {
124
+ if (isComponentMissing) {
123
125
  riverLogger.warning({
124
- message: "Cell Renderer is null - will fallback to default cell",
125
- data: { item, CellRenderer },
126
+ message: `Component ${item.component_type} is null - skipping rendering`,
127
+ });
128
+
129
+ onLoadFinished(index);
130
+ } else {
131
+ riverLogger.log({
132
+ message: "mounting component",
133
+ data: { item, feedUrl, Component, CellRenderer },
126
134
  jsOnly: true,
127
135
  });
136
+
137
+ if (!CellRenderer && !isGroup(item)) {
138
+ riverLogger.warning({
139
+ message: "Cell Renderer is null - will fallback to default cell",
140
+ data: { item, CellRenderer },
141
+ jsOnly: true,
142
+ });
143
+ }
128
144
  }
129
145
  }, []);
130
146
 
131
- if (!readyToBeDisplayed) {
132
- return null;
133
- }
134
-
135
- if (Component === null || typeof Component === "undefined") {
136
- riverLogger.warning({
137
- message: `Component ${item.component_type} is null - skipping rendering`,
138
- });
139
-
140
- onLoadFinished(index);
141
-
147
+ if (!readyToBeDisplayed || isComponentMissing) {
142
148
  return null;
143
149
  }
144
150
 
@@ -0,0 +1,132 @@
1
+ import { act, renderHook } from "@testing-library/react-native";
2
+ import { refreshCoordinator } from "@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator";
3
+
4
+ import { usePullToRefresh } from "../usePullToRefresh";
5
+
6
+ jest.mock(
7
+ "@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator",
8
+ () => ({
9
+ refreshCoordinator: {
10
+ triggerRefresh: jest.fn(),
11
+ },
12
+ })
13
+ );
14
+
15
+ jest.useFakeTimers();
16
+
17
+ describe("usePullToRefresh", () => {
18
+ const screenId = "test-screen";
19
+ const components = [{ id: "comp1" }, { id: "comp2" }] as any;
20
+
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ });
24
+
25
+ it("should initialize with refreshing=false", () => {
26
+ const { result } = renderHook(() => usePullToRefresh(screenId, components));
27
+
28
+ expect(result.current.refreshing).toBe(false);
29
+ });
30
+
31
+ it("should trigger refresh and set refreshing=true", () => {
32
+ const { result } = renderHook(() => usePullToRefresh(screenId, components));
33
+
34
+ act(() => {
35
+ result.current.onRefresh();
36
+ });
37
+
38
+ expect(result.current.refreshing).toBe(true);
39
+
40
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledWith(
41
+ components,
42
+ screenId
43
+ );
44
+ });
45
+
46
+ it("should set refreshing=false after spinner duration", () => {
47
+ const { result } = renderHook(() => usePullToRefresh(screenId, components));
48
+
49
+ act(() => {
50
+ result.current.onRefresh();
51
+ });
52
+
53
+ expect(result.current.refreshing).toBe(true);
54
+
55
+ act(() => {
56
+ jest.advanceTimersByTime(1500);
57
+ });
58
+
59
+ expect(result.current.refreshing).toBe(false);
60
+ });
61
+
62
+ it("should not stop refreshing before spinner duration", () => {
63
+ const { result } = renderHook(() => usePullToRefresh(screenId, components));
64
+
65
+ act(() => {
66
+ result.current.onRefresh();
67
+ });
68
+
69
+ act(() => {
70
+ jest.advanceTimersByTime(1000);
71
+ });
72
+
73
+ expect(result.current.refreshing).toBe(true);
74
+ });
75
+
76
+ it("should clear timeout on unmount", () => {
77
+ const clearTimeoutSpy = jest.spyOn(global, "clearTimeout");
78
+
79
+ const { result, unmount } = renderHook(() =>
80
+ usePullToRefresh(screenId, components)
81
+ );
82
+
83
+ act(() => {
84
+ result.current.onRefresh();
85
+ });
86
+
87
+ unmount();
88
+
89
+ expect(clearTimeoutSpy).toHaveBeenCalled();
90
+ });
91
+
92
+ it("should handle multiple refresh calls correctly", () => {
93
+ const { result } = renderHook(() => usePullToRefresh(screenId, components));
94
+
95
+ act(() => {
96
+ result.current.onRefresh();
97
+ result.current.onRefresh();
98
+ });
99
+
100
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(2);
101
+ expect(result.current.refreshing).toBe(true);
102
+
103
+ act(() => {
104
+ jest.advanceTimersByTime(1500);
105
+ });
106
+
107
+ expect(result.current.refreshing).toBe(false);
108
+ });
109
+
110
+ it("should use latest props in callback", () => {
111
+ const { result, rerender } = renderHook(
112
+ ({ screenId, components }) => usePullToRefresh(screenId, components),
113
+ {
114
+ initialProps: { screenId, components },
115
+ }
116
+ );
117
+
118
+ const newComponents = [{ id: "new-comp" }] as any;
119
+ const newScreenId = "new-screen";
120
+
121
+ rerender({ screenId: newScreenId, components: newComponents });
122
+
123
+ act(() => {
124
+ result.current.onRefresh();
125
+ });
126
+
127
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledWith(
128
+ newComponents,
129
+ newScreenId
130
+ );
131
+ });
132
+ });
@@ -0,0 +1 @@
1
+ export { usePullToRefresh } from "./usePullToRefresh";
@@ -0,0 +1,51 @@
1
+ import { useState, useCallback, useRef, useEffect } from "react";
2
+ import { refreshCoordinator } from "@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator";
3
+
4
+ const SPINNER_DURATION_MS = 1500;
5
+
6
+ /**
7
+ * Pull-to-refresh hook.
8
+ *
9
+ * Triggers a refresh for all screen components via RefreshCoordinator.
10
+ * Each component's UrlFeedResolver already subscribes to refresh$ events
11
+ * and calls reloadData() when triggered — so this hook only needs to:
12
+ * 1. Push events into the refresh bus
13
+ * 2. Show a fixed-duration spinner as UX feedback
14
+ *
15
+ * Data updates arrive reactively via Redux (silentRefresh: true keeps
16
+ * old data visible while loading).
17
+ */
18
+ export const usePullToRefresh = (
19
+ screenId: string,
20
+ components: ZappUIComponent[] = []
21
+ ) => {
22
+ const [refreshing, setRefreshing] = useState(false);
23
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
24
+
25
+ // Cleanup timer on unmount
26
+ useEffect(() => {
27
+ return () => {
28
+ if (timerRef.current) {
29
+ clearTimeout(timerRef.current);
30
+ }
31
+ };
32
+ }, []);
33
+
34
+ const onRefresh = useCallback(() => {
35
+ setRefreshing(true);
36
+ refreshCoordinator.triggerRefresh(components, screenId);
37
+
38
+ // Spinner is UX feedback for the gesture.
39
+ // Data updates arrive reactively via Redux (silentRefresh: true).
40
+ if (timerRef.current) {
41
+ clearTimeout(timerRef.current);
42
+ }
43
+
44
+ timerRef.current = setTimeout(
45
+ () => setRefreshing(false),
46
+ SPINNER_DURATION_MS
47
+ );
48
+ }, [components, screenId]);
49
+
50
+ return { refreshing, onRefresh };
51
+ };
@@ -11,6 +11,7 @@ const mockIsOrientationCompatible = jest.fn(() => true);
11
11
  jest.mock("react-native-safe-area-context", () => ({
12
12
  ...jest.requireActual("react-native-safe-area-context"),
13
13
  useSafeAreaInsets: () => ({ top: 44 }),
14
+ useSafeAreaFrame: () => ({ x: 0, y: 0, width: 375, height: 812 }),
14
15
  }));
15
16
 
16
17
  jest.mock("../../RouteManager", () => ({
@@ -5,15 +5,85 @@ import {
5
5
  } from "@applicaster/zapp-react-native-utils/appUtils/orientationHelper";
6
6
  import {
7
7
  useCurrentScreenData,
8
- useDimensions,
9
8
  useRoute,
10
9
  useIsTablet,
10
+ useIsScreenActive,
11
11
  } from "@applicaster/zapp-react-native-utils/reactHooks";
12
12
  import { useMemo, useEffect, useState } from "react";
13
+ import { useSafeAreaFrame } from "react-native-safe-area-context";
14
+
15
+ type MemoizedSafeAreaFrameWithActiveStateOptions = {
16
+ updateForInactiveScreens?: boolean;
17
+ isActive: boolean;
18
+ };
19
+
20
+ /**
21
+ * Base hook that wraps useSafeAreaFrame with memoization and inactive screen filtering.
22
+ * Requires isActive to be passed explicitly - use useMemoizedSafeAreaFrame for automatic detection.
23
+ *
24
+ * @param options.updateForInactiveScreens - If false, frame won't update when screen is inactive (default: true)
25
+ * @param options.isActive - Whether the screen is currently active
26
+ * @returns The memoized safe area frame { x, y, width, height }
27
+ */
28
+ export const useMemoizedSafeAreaFrameWithActiveState = (
29
+ options: MemoizedSafeAreaFrameWithActiveStateOptions
30
+ ) => {
31
+ const { updateForInactiveScreens = true, isActive } = options;
32
+ const frame = useSafeAreaFrame();
33
+
34
+ const [memoFrame, setMemoFrame] = useState(frame);
35
+
36
+ useEffect(() => {
37
+ const shouldUpdate = isActive || updateForInactiveScreens;
38
+
39
+ const dimensionsChanged =
40
+ frame.width !== memoFrame.width || frame.height !== memoFrame.height;
41
+
42
+ if (shouldUpdate && dimensionsChanged) {
43
+ setMemoFrame(frame);
44
+ }
45
+ }, [
46
+ frame.width,
47
+ frame.height,
48
+ isActive,
49
+ updateForInactiveScreens,
50
+ frame,
51
+ memoFrame.width,
52
+ memoFrame.height,
53
+ ]);
54
+
55
+ return memoFrame;
56
+ };
57
+
58
+ type MemoizedSafeAreaFrameOptions = {
59
+ updateForInactiveScreens?: boolean;
60
+ };
61
+
62
+ /**
63
+ * Hook that wraps useSafeAreaFrame with memoization and inactive screen filtering.
64
+ * Uses useIsScreenActive() internally to determine active state - use useMemoizedSafeAreaFrameWithActiveState
65
+ * if you need to pass isActive explicitly.
66
+ *
67
+ * @param options.updateForInactiveScreens - If false, frame won't update when screen is inactive (default: true)
68
+ * @returns The memoized safe area frame { x, y, width, height }
69
+ */
70
+ export const useMemoizedSafeAreaFrame = (
71
+ options: MemoizedSafeAreaFrameOptions = {}
72
+ ) => {
73
+ const { updateForInactiveScreens = true } = options;
74
+ const isActive = useIsScreenActive();
75
+
76
+ return useMemoizedSafeAreaFrameWithActiveState({
77
+ updateForInactiveScreens,
78
+ isActive,
79
+ });
80
+ };
13
81
 
14
82
  export const useWaitForValidOrientation = () => {
15
- const { width: screenWidth, height } = useDimensions("screen", {
16
- fullDimensions: true,
83
+ // Use memoized safe area frame to synchronize with Scene's dimension source
84
+ // This prevents race conditions where the orientation check passes before
85
+ // Scene's memoFrame has updated to the new dimensions
86
+ const { width: screenWidth, height } = useMemoizedSafeAreaFrame({
17
87
  updateForInactiveScreens: false,
18
88
  });
19
89
 
@@ -92,7 +92,13 @@ export function Screen(_props: Props) {
92
92
  const isActive = useIsScreenActive();
93
93
 
94
94
  const ref = React.useRef(null);
95
- const isReady = useWaitForValidOrientation();
95
+ const isOrientationReady = useWaitForValidOrientation();
96
+
97
+ // Playable screens handle their own orientation via the native player plugin,
98
+ // so we skip the orientation wait gate to avoid a deadlock where the screen
99
+ // waits for landscape but blocks rendering that would trigger the rotation.
100
+ const isPlayableRoute = pathname?.includes("/playable");
101
+ const isReady = isOrientationReady || isPlayableRoute;
96
102
 
97
103
  React.useEffect(() => {
98
104
  if (ref.current && isActive && isReady) {
@@ -0,0 +1,46 @@
1
+ import React, { useEffect } from "react";
2
+ import { PreloaderWrapper } from "../PreloaderWrapper";
3
+ import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
4
+ import { useFeedLoader } from "@applicaster/zapp-react-native-utils/reactHooks";
5
+
6
+ import { componentsLogger } from "../../Helpers/logger";
7
+
8
+ const logger = componentsLogger.addSubsystem("ScreenFeedLoader");
9
+
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 }) => {
14
+ const { source: feedUrl, mapping } = feedData;
15
+
16
+ const { data, loading, error } = useFeedLoader({
17
+ feedUrl,
18
+ mapping,
19
+ pipesOptions: {},
20
+ });
21
+
22
+ const feedStore = useScreenContextV2()._feedStore;
23
+
24
+ useEffect(() => {
25
+ if (data && !loading) {
26
+ feedStore.setState({ screenFeed: data, screenFeedError: null });
27
+
28
+ logger.log("screenFeed set for active screen", { data, screenId: id });
29
+ }
30
+
31
+ if (error && !loading) {
32
+ feedStore.setState({ screenFeed: data, screenFeedError: error });
33
+
34
+ logger.warning("Feed data error:", {
35
+ data,
36
+ loading,
37
+ error,
38
+ screenId: id,
39
+ });
40
+ }
41
+ }, [data, loading, error, feedStore, id]);
42
+
43
+ return (
44
+ <PreloaderWrapper showPreloader={loading}>{children}</PreloaderWrapper>
45
+ );
46
+ };
@@ -0,0 +1,94 @@
1
+ import React from "react";
2
+ import { Text } from "react-native";
3
+ import { render, waitFor } from "@testing-library/react-native";
4
+ import { ScreenFeedLoader } from "../ScreenFeedLoader";
5
+
6
+ const mockUseFeedLoader = jest.fn();
7
+ const mockUseScreenContextV2 = jest.fn();
8
+ const mockSetState = jest.fn();
9
+
10
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
11
+ useFeedLoader: (...args) => mockUseFeedLoader(...args),
12
+ }));
13
+
14
+ jest.mock(
15
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext",
16
+ () => ({
17
+ useScreenContextV2: (...args) => mockUseScreenContextV2(...args),
18
+ })
19
+ );
20
+
21
+ describe("ScreenFeedLoader", () => {
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+
25
+ mockUseScreenContextV2.mockReturnValue({
26
+ _feedStore: {
27
+ setState: mockSetState,
28
+ },
29
+ });
30
+ });
31
+
32
+ it("hides children while loading", () => {
33
+ mockUseFeedLoader.mockReturnValue({
34
+ data: null,
35
+ loading: true,
36
+ error: null,
37
+ });
38
+
39
+ const { queryByText } = render(
40
+ <ScreenFeedLoader id="test" feedData={{ source: "url", mapping: {} }}>
41
+ <Text>child</Text>
42
+ </ScreenFeedLoader>
43
+ );
44
+
45
+ expect(queryByText("child")).toBeNull();
46
+ });
47
+
48
+ it("writes loaded feed data to _feedStore", async () => {
49
+ const data = { entry: { id: "1" } };
50
+
51
+ mockUseFeedLoader.mockReturnValue({
52
+ data,
53
+ loading: false,
54
+ error: null,
55
+ });
56
+
57
+ render(
58
+ <ScreenFeedLoader id="test" feedData={{ source: "url", mapping: {} }}>
59
+ <Text>child</Text>
60
+ </ScreenFeedLoader>
61
+ );
62
+
63
+ await waitFor(() => {
64
+ expect(mockSetState).toHaveBeenCalledWith({
65
+ screenFeed: data,
66
+ screenFeedError: null,
67
+ });
68
+ });
69
+ });
70
+
71
+ it("writes feed error to _feedStore", async () => {
72
+ const error = new Error("feed failed");
73
+ const data = { fallback: true };
74
+
75
+ mockUseFeedLoader.mockReturnValue({
76
+ data,
77
+ loading: false,
78
+ error,
79
+ });
80
+
81
+ render(
82
+ <ScreenFeedLoader id="test" feedData={{ source: "url", mapping: {} }}>
83
+ <Text>child</Text>
84
+ </ScreenFeedLoader>
85
+ );
86
+
87
+ await waitFor(() => {
88
+ expect(mockSetState).toHaveBeenCalledWith({
89
+ screenFeed: data,
90
+ screenFeedError: error,
91
+ });
92
+ });
93
+ });
94
+ });
@@ -0,0 +1 @@
1
+ export { ScreenFeedLoader } from "./ScreenFeedLoader";
@@ -53,6 +53,9 @@ const mockComponents = { ScreenType1, ScreenType2, PlayerController };
53
53
 
54
54
  const mockState = {
55
55
  components: mockComponents,
56
+ remoteConfigurations: {
57
+ assets: {},
58
+ },
56
59
  plugins: [
57
60
  mockScreenType3,
58
61
  mockScreenType4,
@@ -127,6 +130,21 @@ const getWrapper = (screenId, screenType, screenData) => {
127
130
  );
128
131
  };
129
132
 
133
+ const getWrappedWrapper = (screenId, screenType, screenData) => {
134
+ const ScreenResolver = require("../").ScreenResolver;
135
+
136
+ return renderWithProviders(
137
+ <ScreenResolver
138
+ {...{
139
+ screenId,
140
+ screenType,
141
+ screenData,
142
+ }}
143
+ />,
144
+ mockState
145
+ );
146
+ };
147
+
130
148
  describe("<ScreenResolver />", () => {
131
149
  it("renders correctly", () => {
132
150
  const wrapper = getWrapper("1234", "screen_type_1", {});
@@ -134,6 +152,12 @@ describe("<ScreenResolver />", () => {
134
152
  expect(wrapper.getByTestId("screen_type_1")).toBeDefined();
135
153
  });
136
154
 
155
+ it("renders correctly when wrapped with default screen context", () => {
156
+ const wrapper = getWrappedWrapper("1234", "screen_type_1", {});
157
+
158
+ expect(wrapper.getByTestId("screen_type_1")).toBeDefined();
159
+ });
160
+
137
161
  it("picks screen from plugins if it exists", () => {
138
162
  const wrapper = getWrapper("A1234", "screen_type_3", {});
139
163