@applicaster/zapp-react-native-ui-components 15.0.0-alpha.1257410812 → 15.0.0-alpha.1305114721
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.
- package/Components/GeneralContentScreen/GeneralContentScreen.tsx +39 -28
- package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
- package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
- package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
- package/Components/HandlePlayable/HandlePlayable.tsx +16 -29
- package/Components/HandlePlayable/utils.ts +31 -0
- package/Components/HookRenderer/HookRenderer.tsx +40 -10
- package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
- package/Components/Layout/TV/NavBarContainer.tsx +1 -10
- package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
- package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
- package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
- package/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
- package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +136 -0
- package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +33 -16
- package/Components/MasterCell/DefaultComponents/PressableView.tsx +196 -0
- package/Components/MasterCell/DefaultComponents/Text/index.tsx +2 -6
- package/Components/MasterCell/DefaultComponents/index.ts +2 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +46 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +126 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ButtonContainerView.ts +23 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +32 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +191 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +140 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +222 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +104 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +73 -0
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +86 -0
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +35 -52
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +92 -103
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +37 -148
- package/Components/MasterCell/elementMapper.tsx +1 -0
- package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +17 -9
- package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +16 -8
- package/Components/OfflineHandler/NotificationView/utils.ts +34 -0
- package/Components/PlayerContainer/PlayerContainer.tsx +1 -18
- package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
- package/Components/PreloaderWrapper/index.tsx +15 -0
- package/Components/River/ComponentsMap/ComponentsMap.tsx +16 -0
- package/Components/River/RefreshControl.tsx +36 -13
- package/Components/River/RiverItem.tsx +26 -20
- package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
- package/Components/River/__tests__/componentsMap.test.js +38 -0
- package/Components/Screen/__tests__/Screen.test.tsx +1 -0
- package/Components/Screen/hooks.ts +73 -3
- package/Components/Screen/index.tsx +7 -1
- package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
- package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
- package/Components/ScreenFeedLoader/index.ts +1 -0
- package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
- package/Components/ScreenResolver/hooks/index.ts +3 -0
- package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
- package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
- package/Components/ScreenResolver/index.tsx +15 -117
- package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
- package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
- package/Components/ScreenResolver/utils/index.ts +1 -0
- package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
- package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
- package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
- package/Components/ScreenResolverFeedProvider/index.ts +1 -0
- package/Components/Tabs/TabContent.tsx +7 -4
- package/Components/Transitioner/Scene.tsx +9 -15
- package/Components/VideoLive/LiveImageManager.ts +199 -54
- package/Components/VideoLive/PlayerLiveImageComponent.tsx +31 -33
- package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
- package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
- package/Components/Viewport/ViewportAware/index.tsx +16 -7
- package/Components/ZappUIComponent/index.tsx +12 -6
- package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
- package/Components/index.js +1 -1
- package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
- package/Contexts/ScreenContext/index.tsx +71 -19
- package/Contexts/ZappPipesContext/ZappPipesContextFactory.tsx +18 -7
- package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -7
- package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +212 -5
- package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
- package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
- package/events/index.ts +2 -0
- package/events/scrollEndReached.ts +15 -0
- package/package.json +5 -5
- package/Components/MasterCell/DefaultComponents/Text/utils/__tests__/withAdjustedLineHeight.test.ts +0 -46
- package/Components/MasterCell/DefaultComponents/Text/utils/index.ts +0 -21
- package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
- package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
- package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +0 -101
- /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import { render } from "@testing-library/react-native";
|
|
4
|
+
import { PreloaderWrapper } from "..";
|
|
5
|
+
|
|
6
|
+
describe("PreloaderWrapper", () => {
|
|
7
|
+
it("renders children when preloader is hidden", () => {
|
|
8
|
+
const { getByText } = render(
|
|
9
|
+
<PreloaderWrapper showPreloader={false}>
|
|
10
|
+
<Text>content</Text>
|
|
11
|
+
</PreloaderWrapper>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(getByText("content")).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("renders null when preloader is shown", () => {
|
|
18
|
+
const { queryByText } = render(
|
|
19
|
+
<PreloaderWrapper showPreloader>
|
|
20
|
+
<Text>content</Text>
|
|
21
|
+
</PreloaderWrapper>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(queryByText("content")).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
type PreloaderWrapperProps = {
|
|
4
|
+
showPreloader?: boolean;
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const PreloaderWrapper: React.FC<PreloaderWrapperProps> = ({
|
|
9
|
+
showPreloader = false,
|
|
10
|
+
children,
|
|
11
|
+
}) => {
|
|
12
|
+
return !showPreloader ? children : null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default PreloaderWrapper;
|
|
@@ -23,6 +23,7 @@ import { isLast } from "@applicaster/zapp-react-native-utils/arrayUtils";
|
|
|
23
23
|
import { withComponentsMapProvider } from "@applicaster/zapp-react-native-ui-components/Decorators/ComponentsMapWrapper";
|
|
24
24
|
import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
|
|
25
25
|
import { useShallow } from "zustand/react/shallow";
|
|
26
|
+
import { emitScrollEndReached } from "@applicaster/zapp-react-native-ui-components/events";
|
|
26
27
|
|
|
27
28
|
import { isAndroidPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
28
29
|
import { ComponentsMapHeightContext } from "./ContextProviders/ComponentsMapHeightContext";
|
|
@@ -73,6 +74,7 @@ function ComponentsMapComponent(props: Props) {
|
|
|
73
74
|
|
|
74
75
|
const flatListRef = React.useRef<FlatList | null>(null);
|
|
75
76
|
const flatListWrapperRef = React.useRef<View | null>(null);
|
|
77
|
+
const hasUserScrolledRef = React.useRef(false);
|
|
76
78
|
const screenConfig = useScreenConfiguration(riverId);
|
|
77
79
|
const screenData = useScreenData(riverId);
|
|
78
80
|
const pullToRefreshEnabled = screenData?.rules?.pull_to_refresh_enabled;
|
|
@@ -236,6 +238,8 @@ function ComponentsMapComponent(props: Props) {
|
|
|
236
238
|
}, []);
|
|
237
239
|
|
|
238
240
|
const onScroll = React.useCallback((event) => {
|
|
241
|
+
hasUserScrolledRef.current = true;
|
|
242
|
+
|
|
239
243
|
const {
|
|
240
244
|
nativeEvent: {
|
|
241
245
|
contentOffset: { y },
|
|
@@ -277,6 +281,7 @@ function ComponentsMapComponent(props: Props) {
|
|
|
277
281
|
>
|
|
278
282
|
<ViewportTracker>
|
|
279
283
|
<FlatList
|
|
284
|
+
testID="components-map-flat-list"
|
|
280
285
|
ref={(ref) => {
|
|
281
286
|
flatListRef.current = ref;
|
|
282
287
|
}}
|
|
@@ -308,6 +313,17 @@ function ComponentsMapComponent(props: Props) {
|
|
|
308
313
|
onScrollEndDrag={_onScrollEndDrag}
|
|
309
314
|
scrollEventThrottle={16}
|
|
310
315
|
{...scrollViewExtraProps}
|
|
316
|
+
onEndReached={
|
|
317
|
+
/* When wrapped in a parent ScrollView (e.g. tabs),
|
|
318
|
+
this FlatList doesn't scroll so onEndReached can fire repeatedly;
|
|
319
|
+
skip it here and let the parent ScrollView emit scroll-end instead. */
|
|
320
|
+
isScreenWrappedInContainer
|
|
321
|
+
? undefined
|
|
322
|
+
: () => {
|
|
323
|
+
if (!hasUserScrolledRef.current) return;
|
|
324
|
+
emitScrollEndReached();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
311
327
|
/>
|
|
312
328
|
</ViewportTracker>
|
|
313
329
|
</ScreenLoadingMeasurements>
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
StyleSheet,
|
|
6
6
|
} from "react-native";
|
|
7
7
|
import * as R from "ramda";
|
|
8
|
+
import { path } from "@applicaster/zapp-react-native-utils/utils";
|
|
9
|
+
import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
|
|
8
10
|
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
9
11
|
import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils";
|
|
10
12
|
import { useAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils";
|
|
@@ -14,6 +16,7 @@ import { useShallow } from "zustand/react/shallow";
|
|
|
14
16
|
import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
|
|
15
17
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
16
18
|
import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
19
|
+
import { useGetUrlInflater } from "@applicaster/zapp-react-native-utils/reactHooks/feed/useInflatedUrl";
|
|
17
20
|
|
|
18
21
|
const BRIGHTNESS_THRESHOLD = 160;
|
|
19
22
|
const ABOVE_DEFAULT_COLOR = "gray";
|
|
@@ -62,31 +65,51 @@ export const usePullToRefresh = (
|
|
|
62
65
|
|
|
63
66
|
const [refreshing, setRefreshing] = React.useState(false);
|
|
64
67
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const feedSources = React.useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
(riverComponents || [])
|
|
71
|
+
.map((riverComponent) => ({
|
|
72
|
+
source: path(["data", "source"], riverComponent),
|
|
73
|
+
mapping: path(["data", "mapping"], riverComponent),
|
|
74
|
+
}))
|
|
75
|
+
.filter(({ source }) => !isNilOrEmpty(source)),
|
|
76
|
+
[riverComponents]
|
|
77
|
+
);
|
|
68
78
|
|
|
69
|
-
const feedsLength =
|
|
79
|
+
const feedsLength = feedSources.length;
|
|
70
80
|
|
|
71
81
|
const [requestsCompletedCounter, setRequestsCompletedCounter] =
|
|
72
82
|
React.useState(0);
|
|
73
83
|
|
|
74
84
|
const loadPipesDataDispatcher = useLoadPipesDataDispatch();
|
|
85
|
+
const urlInflater = useGetUrlInflater();
|
|
75
86
|
|
|
76
87
|
React.useEffect(() => {
|
|
77
88
|
// will not work for pipes v1 on 1st level screens
|
|
78
89
|
if (refreshing && !isPipesV1) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
feedSources.forEach(({ source, mapping }) => {
|
|
91
|
+
const inflatedUrl = urlInflater(source, mapping);
|
|
92
|
+
|
|
93
|
+
if (inflatedUrl) {
|
|
94
|
+
loadPipesDataDispatcher(inflatedUrl, {
|
|
95
|
+
silentRefresh: true,
|
|
96
|
+
clearCache: true,
|
|
97
|
+
callback: () => {
|
|
98
|
+
setRequestsCompletedCounter(R.inc);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
setRequestsCompletedCounter(R.inc);
|
|
103
|
+
}
|
|
87
104
|
});
|
|
88
105
|
}
|
|
89
|
-
}, [
|
|
106
|
+
}, [
|
|
107
|
+
refreshing,
|
|
108
|
+
isPipesV1,
|
|
109
|
+
feedSources,
|
|
110
|
+
loadPipesDataDispatcher,
|
|
111
|
+
urlInflater,
|
|
112
|
+
]);
|
|
90
113
|
|
|
91
114
|
React.useEffect(() => {
|
|
92
115
|
if (requestsCompletedCounter === feedsLength) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
125
|
-
|
|
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
|
|
|
@@ -137,6 +137,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
|
|
|
137
137
|
keyExtractor={[Function]}
|
|
138
138
|
maxToRenderPerBatch={10}
|
|
139
139
|
onContentSizeChange={[Function]}
|
|
140
|
+
onEndReached={[Function]}
|
|
140
141
|
onLayout={[Function]}
|
|
141
142
|
onMomentumScrollBegin={[Function]}
|
|
142
143
|
onMomentumScrollEnd={[Function]}
|
|
@@ -154,6 +155,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
|
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
stickyHeaderIndices={[]}
|
|
158
|
+
testID="components-map-flat-list"
|
|
157
159
|
viewabilityConfigCallbackPairs={[]}
|
|
158
160
|
windowSize={12}
|
|
159
161
|
>
|
|
@@ -139,7 +139,13 @@ jest.mock(
|
|
|
139
139
|
})
|
|
140
140
|
);
|
|
141
141
|
|
|
142
|
+
jest.mock("@applicaster/zapp-react-native-ui-components/events", () => ({
|
|
143
|
+
...jest.requireActual("@applicaster/zapp-react-native-ui-components/events"),
|
|
144
|
+
emitScrollEndReached: jest.fn(),
|
|
145
|
+
}));
|
|
146
|
+
|
|
142
147
|
const { View } = require("react-native");
|
|
148
|
+
const events = require("@applicaster/zapp-react-native-ui-components/events");
|
|
143
149
|
const { ComponentsMap } = require("../ComponentsMap/ComponentsMap");
|
|
144
150
|
const theme = require("./theme-mock.json");
|
|
145
151
|
|
|
@@ -190,4 +196,36 @@ describe("componentsMap", () => {
|
|
|
190
196
|
|
|
191
197
|
expect(toJSON()).toMatchSnapshot();
|
|
192
198
|
});
|
|
199
|
+
|
|
200
|
+
it("calls emitScrollEndReached when onScroll was called and isScreenWrappedInContainer is false", () => {
|
|
201
|
+
themeSpy = jest
|
|
202
|
+
.spyOn(themeUtils, "useTheme")
|
|
203
|
+
.mockImplementation(() => () => theme);
|
|
204
|
+
|
|
205
|
+
events.emitScrollEndReached.mockClear();
|
|
206
|
+
|
|
207
|
+
const { getByTestId } = render(
|
|
208
|
+
<Provider store={store}>
|
|
209
|
+
<ComponentsMap
|
|
210
|
+
{...props}
|
|
211
|
+
isScreenWrappedInContainer={false}
|
|
212
|
+
feed={{ entry: [] }}
|
|
213
|
+
/>
|
|
214
|
+
</Provider>
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const flatList = getByTestId("components-map-flat-list");
|
|
218
|
+
|
|
219
|
+
flatList.props.onScroll({
|
|
220
|
+
nativeEvent: {
|
|
221
|
+
contentOffset: { y: 0 },
|
|
222
|
+
layoutMeasurement: { height: 100 },
|
|
223
|
+
contentSize: { height: 200 },
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
flatList.props.onEndReached();
|
|
228
|
+
|
|
229
|
+
expect(events.emitScrollEndReached).toHaveBeenCalledTimes(1);
|
|
230
|
+
});
|
|
193
231
|
});
|
|
@@ -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
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { toPascalCase } from "@applicaster/zapp-react-native-utils/stringUtils";
|
|
4
|
+
import {
|
|
5
|
+
useAppSelector,
|
|
6
|
+
selectComponents,
|
|
7
|
+
} from "@applicaster/zapp-react-native-redux";
|
|
8
|
+
|
|
9
|
+
export const useGetComponent = (screenType) => {
|
|
10
|
+
const components = useAppSelector(selectComponents);
|
|
11
|
+
|
|
12
|
+
return React.useMemo(() => {
|
|
13
|
+
return components[toPascalCase(screenType)];
|
|
14
|
+
}, [components, screenType]);
|
|
15
|
+
};
|