@applicaster/zapp-react-native-ui-components 15.0.0-alpha.5104105031 → 15.0.0-alpha.5219062121

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.
@@ -5,6 +5,14 @@ import {
5
5
 
6
6
  import { CHROMECAST_PLUGIN_ID, YOUTUBE_PLUGIN_ID } from "./const";
7
7
  import { omit } from "@applicaster/zapp-react-native-utils/utils";
8
+ import { getXray } from "@applicaster/zapp-react-native-utils/logger";
9
+
10
+ const { Logger } = getXray();
11
+
12
+ const logger = new Logger(
13
+ "QuickBrick",
14
+ "packages/zapp-react-native-ui-components/Components/HandlePlayable"
15
+ );
8
16
 
9
17
  const getPlayerModuleProperties = (PlayerModule: ZappPlugin) => {
10
18
  if (PlayerModule?.Component && typeof PlayerModule.Component === "object") {
@@ -52,10 +60,25 @@ export const getPlayer = (
52
60
  if (type) {
53
61
  PlayerModule = findPluginByIdentifier(type, plugins)?.module;
54
62
 
63
+ if (!PlayerModule) {
64
+ logger.error({
65
+ message:
66
+ "PlayerModule is undefined – type mapping may be wrong or type not set for player",
67
+ data: {
68
+ type,
69
+ screen_id,
70
+ item_type_value: item?.type?.value,
71
+ },
72
+ });
73
+
74
+ return [null, {}];
75
+ }
76
+
55
77
  return getPlayerWithModuleProperties(PlayerModule);
56
78
  }
57
79
  }
58
80
 
81
+ // TODO: Probably should be removed, Youtube plugin is deprecated
59
82
  if (item?.content?.type === "youtube-id") {
60
83
  PlayerModule = findYoutubePlugin(plugins)?.module;
61
84
 
@@ -70,5 +93,13 @@ export const getPlayer = (
70
93
  )
71
94
  );
72
95
 
96
+ if (!PlayerModule) {
97
+ logger.error({
98
+ message: "PlayerModule is undefined – playable plugin not found",
99
+ });
100
+
101
+ return [null, {}];
102
+ }
103
+
73
104
  return getPlayerWithModuleProperties(PlayerModule);
74
105
  };
@@ -277,7 +277,6 @@ const PlayerContainerComponent = (props: Props) => {
277
277
  const { isRestricted } = useRestrictMobilePlayback({
278
278
  player,
279
279
  entry: item,
280
- pluginConfiguration,
281
280
  close,
282
281
  });
283
282
 
@@ -670,7 +669,7 @@ const PlayerContainerComponent = (props: Props) => {
670
669
  <PlayerFocusableWrapperView
671
670
  nextFocusDown={context.bottomFocusableId}
672
671
  >
673
- {isRestricted ? null : (
672
+ {!Player || isRestricted ? null : (
674
673
  <Player
675
674
  source={{
676
675
  uri,
@@ -1,27 +1,50 @@
1
1
  import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
2
2
  import NetInfo from "@react-native-community/netinfo";
3
3
 
4
- import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { showAlertDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
6
6
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
7
7
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
8
+ import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
9
+ import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
10
+ import { getLocalizations } from "@applicaster/zapp-react-native-utils/localizationUtils";
8
11
  import { log_info } from "./logger";
12
+ import { isTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
9
13
 
10
14
  type RestrictMobilePlaybackProps = {
11
15
  player?: Player;
12
16
  entry?: ZappEntry;
13
- pluginConfiguration?: Record<string, string>;
14
17
  close: () => void;
15
18
  };
16
19
 
17
20
  export const useRestrictMobilePlayback = ({
18
21
  player,
19
22
  entry,
20
- pluginConfiguration,
21
23
  close,
22
24
  }: RestrictMobilePlaybackProps): { isRestricted: boolean } => {
23
25
  const dialogVisibleRef = useRef<boolean>(false);
24
26
  const theme = useTheme();
27
+ const plugins = usePlugins();
28
+
29
+ const restrictMobilePlugin = useMemo(
30
+ () =>
31
+ plugins.find((p) => p.identifier === "quick-brick-hook-restrict-mobile"),
32
+ [plugins]
33
+ );
34
+
35
+ const localizations = useMemo(
36
+ () => restrictMobilePlugin?.configuration?.localizations,
37
+ [restrictMobilePlugin]
38
+ );
39
+
40
+ const localize = useCallback(
41
+ (key: string) => {
42
+ const l = localizations && getLocalizations({ localizations });
43
+
44
+ return (l && l[key]) || "";
45
+ },
46
+ [localizations]
47
+ );
25
48
 
26
49
  useEffect(() => {
27
50
  return () => {
@@ -38,8 +61,13 @@ export const useRestrictMobilePlayback = ({
38
61
  return false;
39
62
  }
40
63
 
41
- return player && entry?.extensions?.connection_restricted;
42
- }, [player, entry]);
64
+ // Only restrict if the plugin exists
65
+ if (!restrictMobilePlugin) {
66
+ return false;
67
+ }
68
+
69
+ return player && isTrue(entry?.extensions?.connection_restricted);
70
+ }, [player, entry, restrictMobilePlugin]);
43
71
 
44
72
  const [isRestricted, setIsRestricted] = useState<boolean>(
45
73
  isConnectionRestricted
@@ -55,16 +83,17 @@ export const useRestrictMobilePlayback = ({
55
83
  "Stopping player due to mobile restriction, connection_restricted: true"
56
84
  );
57
85
 
58
- player?.close();
86
+ player?.closeNativePlayer();
87
+ playerManager?.invokeHandler("close");
59
88
 
60
89
  dialogVisibleRef.current = true;
61
90
 
62
91
  showAlertDialog({
63
92
  title:
64
- pluginConfiguration?.mobile_connection_restricted_alert_title ||
93
+ localize("restrict_mobile_playback_error_title") ||
65
94
  "Restricted Connection Type",
66
95
  message:
67
- pluginConfiguration?.mobile_connection_restricted_alert_message ||
96
+ localize("restrict_mobile_playback_error_message") ||
68
97
  "This content can only be viewed over a Wi-Fi or LAN network.",
69
98
  okButtonText: theme.ok_button || "OK",
70
99
  completion: () => {
@@ -91,7 +120,8 @@ export const useRestrictMobilePlayback = ({
91
120
  }, [
92
121
  close,
93
122
  entry?.extensions?.connection_restricted,
94
- pluginConfiguration,
123
+ localizations,
124
+ localize,
95
125
  player,
96
126
  theme.ok_button,
97
127
  isConnectionRestricted,
@@ -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>
@@ -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
  });
@@ -32,7 +32,7 @@ function getTestDimensions(testDimensions) {
32
32
  return (process.env.NODE_ENV === "test" && testDimensions) || null;
33
33
  }
34
34
 
35
- function ViewportAwareComponent(props: Props, ref) {
35
+ function ViewportAwareComponent(props: Props, forwardedRef) {
36
36
  const viewportEvents = useViewportEventsContext();
37
37
 
38
38
  const {
@@ -47,13 +47,22 @@ function ViewportAwareComponent(props: Props, ref) {
47
47
  getTestDimensions(testDimensions) || initialDimensions
48
48
  );
49
49
 
50
+ const localRef = React.useRef(null);
51
+
50
52
  const [viewportChangeEvent, setViewportChangeEvent] = React.useState(null);
51
53
 
52
- function assignRef(_ref) {
53
- if (_ref && !ref) {
54
- ref = _ref;
55
- }
56
- }
54
+ const assignRef = React.useCallback(
55
+ (_ref) => {
56
+ localRef.current = _ref;
57
+
58
+ if (typeof forwardedRef === "function") {
59
+ forwardedRef(_ref);
60
+ } else if (forwardedRef) {
61
+ forwardedRef.current = _ref;
62
+ }
63
+ },
64
+ [forwardedRef]
65
+ );
57
66
 
58
67
  const checkInViewport = (viewportChangeEvent, layoutEvent) => {
59
68
  const inVerticalViewport = Utils.isInViewport(
@@ -94,7 +103,7 @@ function ViewportAwareComponent(props: Props, ref) {
94
103
  const onViewportChange = (viewportChangeEvent) => {
95
104
  setViewportChangeEvent(viewportChangeEvent);
96
105
 
97
- const nodeHandle = findNodeHandle(ref);
106
+ const nodeHandle = findNodeHandle(localRef.current);
98
107
 
99
108
  if (!nodeHandle) {
100
109
  return;
package/events/index.ts CHANGED
@@ -7,3 +7,5 @@ export enum QBUIComponentEvents {
7
7
  focusOnSelectedTopMenuItem = "focusOnSelectedTopMenuItem",
8
8
  scrollToTopForScreenWrappedInContainer = "scrollToTopForScreenWrappedInContainer",
9
9
  }
10
+
11
+ export { scrollEndReached$, emitScrollEndReached } from "./scrollEndReached";
@@ -0,0 +1,15 @@
1
+ import { Subject } from "rxjs";
2
+ import { throttleTime } from "rxjs/operators";
3
+
4
+ const SCROLL_END_THROTTLE_MS = 1000;
5
+ const scrollEndReachedSubject = new Subject<void>();
6
+
7
+ /* Throttle so we only emit at most once per second; RN often fires onEndReached repeatedly (e.g. on Android) when near the bottom. */
8
+ export const scrollEndReached$ = scrollEndReachedSubject.pipe(
9
+ throttleTime(SCROLL_END_THROTTLE_MS)
10
+ );
11
+
12
+ /* Call from scroll container (ComponentsMap or Tabs) when scroll reaches end. */
13
+ export const emitScrollEndReached = (): void => {
14
+ scrollEndReachedSubject.next();
15
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "15.0.0-alpha.5104105031",
3
+ "version": "15.0.0-alpha.5219062121",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "15.0.0-alpha.5104105031",
32
- "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.5104105031",
33
- "@applicaster/zapp-react-native-redux": "15.0.0-alpha.5104105031",
34
- "@applicaster/zapp-react-native-utils": "15.0.0-alpha.5104105031",
31
+ "@applicaster/applicaster-types": "15.0.0-alpha.5219062121",
32
+ "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.5219062121",
33
+ "@applicaster/zapp-react-native-redux": "15.0.0-alpha.5219062121",
34
+ "@applicaster/zapp-react-native-utils": "15.0.0-alpha.5219062121",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",