@applicaster/zapp-react-native-ui-components 15.0.0-alpha.4153840309 → 15.0.0-alpha.4200413542

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 (85) hide show
  1. package/Components/BaseFocusable/index.ios.ts +12 -2
  2. package/Components/Cell/Cell.tsx +6 -0
  3. package/Components/Cell/CellWithFocusable.tsx +9 -0
  4. package/Components/Cell/FocusableWrapper.tsx +3 -0
  5. package/Components/Cell/TvOSCellComponent.tsx +17 -5
  6. package/Components/Focusable/Focusable.tsx +4 -2
  7. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  8. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  9. package/Components/FocusableGroup/FocusableTvOS.tsx +32 -1
  10. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  11. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +22 -6
  12. package/Components/HandlePlayable/HandlePlayable.tsx +10 -7
  13. package/Components/Layout/TV/LayoutBackground.tsx +5 -2
  14. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  15. package/Components/Layout/TV/index.tsx +3 -4
  16. package/Components/Layout/TV/index.web.tsx +3 -4
  17. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  18. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +4 -10
  19. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +5 -1
  20. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  21. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  22. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  23. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +40 -39
  24. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/Image.test.tsx +95 -0
  25. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +86 -0
  26. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/index.test.ts +141 -0
  27. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  28. package/Components/MasterCell/DefaultComponents/SecondaryImage/index.ts +1 -1
  29. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +6 -2
  30. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +233 -11
  31. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +19 -15
  32. package/Components/MasterCell/hoc/__tests__/withAsyncRender.test.tsx +219 -0
  33. package/Components/MasterCell/hoc/withAsyncRender.tsx +9 -7
  34. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +82 -3
  35. package/Components/MasterCell/utils/index.ts +61 -31
  36. package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +102 -87
  37. package/Components/MeasurmentsPortal/__tests__/MeasurementsPortal.test.tsx +355 -0
  38. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +17 -9
  39. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +16 -8
  40. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  41. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  42. package/Components/OfflineHandler/NotificationView/utils.ts +34 -0
  43. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  44. package/Components/PlayerContainer/PlayerContainer.tsx +51 -40
  45. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +101 -0
  46. package/Components/River/ComponentsMap/ComponentsMap.tsx +16 -0
  47. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  48. package/Components/River/TV/River.tsx +31 -14
  49. package/Components/River/TV/index.tsx +8 -4
  50. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  51. package/Components/River/TV/utils/index.ts +4 -0
  52. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  53. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  54. package/Components/River/__tests__/componentsMap.test.js +38 -0
  55. package/Components/Screen/TV/index.web.tsx +4 -2
  56. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  57. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  58. package/Components/Screen/hooks.ts +2 -3
  59. package/Components/Screen/index.tsx +2 -3
  60. package/Components/Screen/navigationHandler.ts +49 -24
  61. package/Components/Screen/orientationHandler.ts +10 -13
  62. package/Components/ScreenResolver/index.tsx +13 -7
  63. package/Components/Tabs/TabContent.tsx +7 -4
  64. package/Components/Transitioner/index.js +3 -3
  65. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +13 -9
  66. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  67. package/Components/VideoModal/utils.ts +12 -9
  68. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  69. package/Contexts/ScreenContext/index.tsx +25 -18
  70. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  71. package/Contexts/ZappHookModalContext/index.tsx +37 -61
  72. package/Contexts/index.ts +0 -2
  73. package/Decorators/Analytics/index.tsx +6 -5
  74. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  75. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  76. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  77. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  78. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  79. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  80. package/Helpers/DataSourceHelper/index.ts +19 -0
  81. package/events/index.ts +3 -0
  82. package/events/scrollEndReached.ts +15 -0
  83. package/package.json +6 -5
  84. package/Components/River/TV/withTVEventHandler.tsx +0 -27
  85. package/Helpers/DataSourceHelper/index.js +0 -19
@@ -1,4 +1,6 @@
1
1
  import * as React from "react";
2
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
3
+ import { useNetworkStatusLocalizations } from "./utils";
2
4
 
3
5
  type Props = {
4
6
  children?: React.ReactNode;
@@ -10,13 +12,6 @@ type Props = {
10
12
 
11
13
  const NOTIF_DURATION = 4500;
12
14
 
13
- export const ONLINE_MSG = "You are back online";
14
-
15
- export const OFFLINE_MSG = "No internet connection";
16
-
17
- const EXTRA_OFFLINE_MSG = "Please check your connection";
18
- const EXTRA_ONLINE_MSG = "Feel free to continue where you left off";
19
-
20
15
  const styles: Record<any, React.CSSProperties> = {
21
16
  body: {
22
17
  position: "absolute",
@@ -47,6 +42,9 @@ const styles: Record<any, React.CSSProperties> = {
47
42
  export const NotificationView = (props: Props) => {
48
43
  const { children, hidden, dismiss, previousOnline, online } = props;
49
44
 
45
+ const theme = useTheme<BaseThemePropertiesTV>();
46
+ const storedLocalizations = useNetworkStatusLocalizations();
47
+
50
48
  const [open, setOpen] = React.useState<boolean | null>(null);
51
49
 
52
50
  const timeout = React.useRef<NodeJS.Timeout>();
@@ -84,8 +82,18 @@ export const NotificationView = (props: Props) => {
84
82
  }, [hidden, online]);
85
83
 
86
84
  const showOnlineMsg = wentOnline || online;
87
- const MSG = showOnlineMsg ? ONLINE_MSG : OFFLINE_MSG;
88
- const EXTRA_MSG = showOnlineMsg ? EXTRA_ONLINE_MSG : EXTRA_OFFLINE_MSG;
85
+
86
+ const MSG = showOnlineMsg
87
+ ? (theme?.online_notification_title ??
88
+ storedLocalizations?.online_notification_title)
89
+ : (theme?.offline_notification_title ??
90
+ storedLocalizations?.offline_notification_title);
91
+
92
+ const EXTRA_MSG = showOnlineMsg
93
+ ? (theme?.online_notification_subtitle ??
94
+ storedLocalizations?.online_notification_subtitle)
95
+ : (theme?.offline_notification_subtitle ??
96
+ storedLocalizations?.offline_notification_subtitle);
89
97
 
90
98
  return (
91
99
  <div onClick={onClose}>
@@ -1,4 +1,6 @@
1
1
  import * as React from "react";
2
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
3
+ import { useNetworkStatusLocalizations } from "./utils";
2
4
 
3
5
  type Props = {
4
6
  children?: React.ReactNode;
@@ -9,12 +11,6 @@ type Props = {
9
11
 
10
12
  const DURATION_TO_HIDE_AFTER_BACK_TO_ONLINE = 4500; // ms
11
13
 
12
- const ONLINE_TITLE = "You are back online";
13
- const ONLINE_SUBTITLE = "Feel free to continue where you left off";
14
-
15
- const OFFLINE_TITLE = "No internet connection";
16
- const OFFLINE_SUBTITLE = "Please check your connection";
17
-
18
14
  const styles: Record<any, React.CSSProperties> = {
19
15
  body: {
20
16
  position: "absolute",
@@ -47,6 +43,9 @@ let timer: NodeJS.Timeout;
47
43
  export const NotificationView = (props: Props) => {
48
44
  const { children, dismiss, previousOnline = true, online } = props;
49
45
 
46
+ const theme = useTheme<BaseThemePropertiesTV>();
47
+ const storedLocalizations = useNetworkStatusLocalizations();
48
+
50
49
  const [shown, setShown] = React.useState<boolean>(false);
51
50
 
52
51
  const onClose = () => {
@@ -79,8 +78,17 @@ export const NotificationView = (props: Props) => {
79
78
  }
80
79
  }, [online, previousOnline]);
81
80
 
82
- const title = online ? ONLINE_TITLE : OFFLINE_TITLE;
83
- const subtitle = online ? ONLINE_SUBTITLE : OFFLINE_SUBTITLE;
81
+ const title = online
82
+ ? (theme?.online_notification_title ??
83
+ storedLocalizations?.online_notification_title)
84
+ : (theme?.offline_notification_title ??
85
+ storedLocalizations?.offline_notification_title);
86
+
87
+ const subtitle = online
88
+ ? (theme?.online_notification_subtitle ??
89
+ storedLocalizations?.online_notification_subtitle)
90
+ : (theme?.offline_notification_subtitle ??
91
+ storedLocalizations?.offline_notification_subtitle);
84
92
 
85
93
  return (
86
94
  <div onClick={onClose}>
@@ -8,7 +8,7 @@ import {
8
8
  TouchableWithoutFeedback,
9
9
  } from "react-native";
10
10
 
11
- import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
11
+ import { usePlugins } from "@applicaster/zapp-react-native-redux";
12
12
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
13
13
  import { textTransform } from "@applicaster/zapp-react-native-utils/cellUtils";
14
14
  import { useNotificationHeight } from "../utils";
@@ -46,7 +46,7 @@ export const NotificationView = ({
46
46
  online = true,
47
47
  dismiss,
48
48
  }: Props) => {
49
- const { plugins } = usePickFromState(["plugins"]);
49
+ const plugins = usePlugins();
50
50
  const { statusHeight, notificationHeight } = useNotificationHeight();
51
51
 
52
52
  const offlinePlugin = R.find(
@@ -8,25 +8,24 @@ import {
8
8
  offlinePhrase,
9
9
  } from "../NotificationView";
10
10
 
11
- jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
12
- usePickFromState: () => ({
13
- plugins: [
14
- {
15
- name: "offline experience",
16
- identifier: "offline-experience",
17
- type: "general",
18
- module: {
19
- useOfflineExperienceConfiguration: () => ({
20
- configurationFields: {},
21
- localizations: {
22
- offline_toast_message: "No internet connection",
23
- online_toast_message: "You are back online",
24
- },
25
- }),
26
- },
11
+ jest.mock("@applicaster/zapp-react-native-redux", () => ({
12
+ ...jest.requireActual("@applicaster/zapp-react-native-redux"),
13
+ usePlugins: () => [
14
+ {
15
+ name: "offline experience",
16
+ identifier: "offline-experience",
17
+ type: "general",
18
+ module: {
19
+ useOfflineExperienceConfiguration: () => ({
20
+ configurationFields: {},
21
+ localizations: {
22
+ offline_toast_message: "No internet connection",
23
+ online_toast_message: "You are back online",
24
+ },
25
+ }),
27
26
  },
28
- ],
29
- }),
27
+ },
28
+ ],
30
29
  }));
31
30
 
32
31
  jest.mock("react-native-safe-area-context", () => ({
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
3
+
4
+ export const NETWORK_STATUS_LOCALIZATIONS_KEY = "network_status_localizations";
5
+
6
+ export const THEME_STORAGE_NAMESPACE = "quick-brick-theme";
7
+
8
+ export function useNetworkStatusLocalizations() {
9
+ const [storedLocalizations, setStoredLocalizations] = React.useState<Record<
10
+ string,
11
+ string
12
+ > | null>(null);
13
+
14
+ React.useEffect(() => {
15
+ async function loadStoredLocalizations() {
16
+ try {
17
+ const stored = await sessionStorage.getItem(
18
+ NETWORK_STATUS_LOCALIZATIONS_KEY,
19
+ THEME_STORAGE_NAMESPACE
20
+ );
21
+
22
+ if (stored) {
23
+ setStoredLocalizations(stored);
24
+ }
25
+ } catch (error) {
26
+ console.error("Error loading network status localizations", error);
27
+ }
28
+ }
29
+
30
+ loadStoredLocalizations();
31
+ }, []);
32
+
33
+ return storedLocalizations;
34
+ }
@@ -8,36 +8,45 @@ const mockPreviousValue = null;
8
8
 
9
9
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/connection", () => {
10
10
  return {
11
+ ...jest.requireActual(
12
+ "@applicaster/zapp-react-native-utils/reactHooks/connection"
13
+ ),
11
14
  useConnectionInfo: jest.fn(() => mockConnectionStatus),
12
15
  };
13
16
  });
14
17
 
15
18
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => {
16
19
  return {
20
+ ...jest.requireActual(
21
+ "@applicaster/zapp-react-native-utils/reactHooks/utils"
22
+ ),
17
23
  usePrevious: jest.fn(() => mockPreviousValue),
18
24
  };
19
25
  });
20
26
 
27
+ const mock_storeObj = {
28
+ plugins: [
29
+ {
30
+ name: "offline experience",
31
+ identifier: "offline-experience",
32
+ type: "general",
33
+ module: {
34
+ useOfflineExperienceConfiguration: () => ({
35
+ configurationFields: {},
36
+ localizations: {
37
+ offline_toast_message: "No internet connection",
38
+ online_toast_message: "You are back online",
39
+ },
40
+ }),
41
+ },
42
+ },
43
+ ],
44
+ };
45
+
21
46
  jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
22
47
  ...jest.requireActual("@applicaster/zapp-react-native-redux/hooks"),
23
- usePickFromState: () => ({
24
- plugins: [
25
- {
26
- name: "offline experience",
27
- identifier: "offline-experience",
28
- type: "general",
29
- module: {
30
- useOfflineExperienceConfiguration: () => ({
31
- configurationFields: {},
32
- localizations: {
33
- offline_toast_message: "No internet connection",
34
- online_toast_message: "You are back online",
35
- },
36
- }),
37
- },
38
- },
39
- ],
40
- }),
48
+ usePlugins: () => mock_storeObj.plugins,
49
+ usePickFromState: () => ({}),
41
50
  }));
42
51
 
43
52
  jest.mock("react-native-safe-area-context", () => ({
@@ -17,7 +17,7 @@ import {
17
17
 
18
18
  import { TVEventHandlerComponent } from "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent";
19
19
  import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
20
- import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
20
+
21
21
  import {
22
22
  useBackHandler,
23
23
  useNavigation,
@@ -60,6 +60,8 @@ import {
60
60
  PlayerNativeCommandTypes,
61
61
  PlayerNativeSendCommand,
62
62
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
63
+ import { useAppData } from "@applicaster/zapp-react-native-redux";
64
+ import { useRestrictMobilePlayback } from "./useRestrictMobilePlayback";
63
65
 
64
66
  type Props = {
65
67
  Player: React.ComponentType<any>;
@@ -238,7 +240,7 @@ const PlayerContainerComponent = (props: Props) => {
238
240
  const [isLoadingNextVideo, setIsLoadingNextVideo] = React.useState(false);
239
241
 
240
242
  const navigator = useNavigation();
241
- const { appData } = usePickFromState(["appData"]);
243
+ const { isTabletPortrait } = useAppData();
242
244
  const prevItemId = usePrevious(item?.id);
243
245
  const screenData = useTargetScreenData(item);
244
246
  const { setVisible: showNavBar } = useSetNavbarState();
@@ -263,7 +265,21 @@ const PlayerContainerComponent = (props: Props) => {
263
265
 
264
266
  showNavBar(true);
265
267
  navigator.goBack();
266
- }, [isModal, navigator.goBack, state.playerId, showNavBar]);
268
+ }, [isModal, state.playerId, showNavBar, navigator]);
269
+
270
+ const pluginConfiguration = React.useMemo(() => {
271
+ return (
272
+ playerManager.getPluginConfiguration() ||
273
+ R.prop("__plugin_configuration", Player)
274
+ );
275
+ }, [playerManager.isRegistered()]);
276
+
277
+ const { isRestricted } = useRestrictMobilePlayback({
278
+ player,
279
+ entry: item,
280
+ pluginConfiguration,
281
+ close,
282
+ });
267
283
 
268
284
  const playEntry = (entry) => navigator.replaceTop(entry, { mode });
269
285
 
@@ -455,13 +471,6 @@ const PlayerContainerComponent = (props: Props) => {
455
471
  }
456
472
  }, []);
457
473
 
458
- const pluginConfiguration = React.useMemo(() => {
459
- return (
460
- playerManager.getPluginConfiguration() ||
461
- R.prop("__plugin_configuration", Player)
462
- );
463
- }, [playerManager.isRegistered()]);
464
-
465
474
  const disableMiniPlayer = React.useMemo(() => {
466
475
  return pluginConfiguration?.disable_mini_player_when_inline;
467
476
  }, [pluginConfiguration]);
@@ -661,36 +670,38 @@ const PlayerContainerComponent = (props: Props) => {
661
670
  <PlayerFocusableWrapperView
662
671
  nextFocusDown={context.bottomFocusableId}
663
672
  >
664
- <Player
665
- source={{
666
- uri,
667
- entry: item,
668
- }}
669
- focused={isInlineTV ? true : undefined}
670
- autoplay={true}
671
- controls={false}
672
- disableCastAction={disableCastAction}
673
- docked={navigator.isVideoModalDocked()}
674
- entry={item}
675
- fullscreen={mode === VideoModalMode.FULLSCREEN}
676
- inline={inline}
677
- isModal={isModal}
678
- isTabletPortrait={appData.isTabletPortrait}
679
- muted={false}
680
- playableItem={item}
681
- playerEvent={playerEvent}
682
- playerId={state.playerId}
683
- pluginConfiguration={pluginConfiguration}
684
- ref={playerRef}
685
- toggleFullscreen={toggleFullscreen}
686
- style={videoStyle}
687
- playNextData={playNextData}
688
- setNextVideoPreloadThresholdPercentage={
689
- setNextVideoPreloadThresholdPercentage
690
- }
691
- >
692
- {renderApplePlayer(applePlayerProps)}
693
- </Player>
673
+ {isRestricted ? null : (
674
+ <Player
675
+ source={{
676
+ uri,
677
+ entry: item,
678
+ }}
679
+ focused={isInlineTV ? true : undefined}
680
+ autoplay={true}
681
+ controls={false}
682
+ disableCastAction={disableCastAction}
683
+ docked={navigator.isVideoModalDocked()}
684
+ entry={item}
685
+ fullscreen={mode === VideoModalMode.FULLSCREEN}
686
+ inline={inline}
687
+ isModal={isModal}
688
+ isTabletPortrait={isTabletPortrait}
689
+ muted={false}
690
+ playableItem={item}
691
+ playerEvent={playerEvent}
692
+ playerId={state.playerId}
693
+ pluginConfiguration={pluginConfiguration}
694
+ ref={playerRef}
695
+ toggleFullscreen={toggleFullscreen}
696
+ style={videoStyle}
697
+ playNextData={playNextData}
698
+ setNextVideoPreloadThresholdPercentage={
699
+ setNextVideoPreloadThresholdPercentage
700
+ }
701
+ >
702
+ {renderApplePlayer(applePlayerProps)}
703
+ </Player>
704
+ )}
694
705
  </PlayerFocusableWrapperView>
695
706
 
696
707
  {state.error ? <ErrorDisplay error={state.error} /> : null}
@@ -0,0 +1,101 @@
1
+ import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
2
+ import NetInfo from "@react-native-community/netinfo";
3
+
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { showAlertDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
6
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
7
+ import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
8
+ import { log_info } from "./logger";
9
+
10
+ type RestrictMobilePlaybackProps = {
11
+ player?: Player;
12
+ entry?: ZappEntry;
13
+ pluginConfiguration?: Record<string, string>;
14
+ close: () => void;
15
+ };
16
+
17
+ export const useRestrictMobilePlayback = ({
18
+ player,
19
+ entry,
20
+ pluginConfiguration,
21
+ close,
22
+ }: RestrictMobilePlaybackProps): { isRestricted: boolean } => {
23
+ const dialogVisibleRef = useRef<boolean>(false);
24
+ const theme = useTheme();
25
+
26
+ useEffect(() => {
27
+ return () => {
28
+ if (isTV()) {
29
+ return;
30
+ }
31
+
32
+ dialogVisibleRef.current = false;
33
+ };
34
+ }, []);
35
+
36
+ const isConnectionRestricted = useMemo(() => {
37
+ if (isTV()) {
38
+ return false;
39
+ }
40
+
41
+ return player && entry?.extensions?.connection_restricted;
42
+ }, [player, entry]);
43
+
44
+ const [isRestricted, setIsRestricted] = useState<boolean>(
45
+ isConnectionRestricted
46
+ );
47
+
48
+ useEffect(() => {
49
+ if (!isConnectionRestricted) {
50
+ return;
51
+ }
52
+
53
+ const stopPlayer = () => {
54
+ log_info(
55
+ "Stopping player due to mobile restriction, connection_restricted: true"
56
+ );
57
+
58
+ player?.close();
59
+
60
+ dialogVisibleRef.current = true;
61
+
62
+ showAlertDialog({
63
+ title:
64
+ pluginConfiguration?.mobile_connection_restricted_alert_title ||
65
+ "Restricted Connection Type",
66
+ message:
67
+ pluginConfiguration?.mobile_connection_restricted_alert_message ||
68
+ "This content can only be viewed over a Wi-Fi or LAN network.",
69
+ okButtonText: theme.ok_button || "OK",
70
+ completion: () => {
71
+ dialogVisibleRef.current = false;
72
+
73
+ close();
74
+ },
75
+ });
76
+ };
77
+
78
+ return NetInfo.addEventListener((state) => {
79
+ if (state.type === "cellular") {
80
+ setIsRestricted(true);
81
+
82
+ if (dialogVisibleRef.current) {
83
+ return;
84
+ }
85
+
86
+ stopPlayer();
87
+ } else {
88
+ setIsRestricted(false);
89
+ }
90
+ });
91
+ }, [
92
+ close,
93
+ entry?.extensions?.connection_restricted,
94
+ pluginConfiguration,
95
+ player,
96
+ theme.ok_button,
97
+ isConnectionRestricted,
98
+ ]);
99
+
100
+ return { isRestricted };
101
+ };
@@ -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>
@@ -1,4 +1,4 @@
1
- import { renderHook, act } from "@testing-library/react-hooks";
1
+ import { renderHook, act } from "@testing-library/react-native";
2
2
  import { BehaviorSubject } from "rxjs";
3
3
  import { useLoadingState } from "../useLoadingState";
4
4
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { Text } from "react-native";
5
- import * as R from "ramda";
5
+
6
+ import { mergeRight } from "@applicaster/zapp-react-native-utils/utils";
6
7
 
7
8
  import { GeneralContentScreen } from "../../GeneralContentScreen";
8
9
  import { ScreenResolver } from "@applicaster/zapp-react-native-ui-components/Components/ScreenResolver";
@@ -13,6 +14,8 @@ import {
13
14
  } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
14
15
  import { useRivers } from "@applicaster/zapp-react-native-utils/reactHooks/state";
15
16
 
17
+ import { toStringOrEmpty } from "./utils";
18
+
16
19
  type Props = {
17
20
  screenId: string;
18
21
  screenData: ZappRiver | ZappEntry;
@@ -24,6 +27,7 @@ type Props = {
24
27
  isInsideContainer?: boolean;
25
28
  extraAnchorPointYOffset: number;
26
29
  river?: ZappRiver | ZappEntry;
30
+ groupId: string;
27
31
  };
28
32
 
29
33
  export const River = (props: Props) => {
@@ -35,6 +39,7 @@ export const River = (props: Props) => {
35
39
  componentsMapExtraProps,
36
40
  isInsideContainer,
37
41
  extraAnchorPointYOffset,
42
+ groupId,
38
43
  } = props;
39
44
 
40
45
  const { title: screenTitle, summary: screenSummary } = useNavbarState();
@@ -51,28 +56,41 @@ export const River = (props: Props) => {
51
56
  [screenId]
52
57
  );
53
58
 
54
- const stringOrEmpty = (value: string | number | undefined): string =>
55
- R.isNil(value) ? "" : String(value);
59
+ const screenResolverData = React.useMemo(() => {
60
+ const extraData = mergeRight(extraProps, screenResolverExtraProps);
61
+
62
+ return {
63
+ extraData,
64
+ screenData: mergeRight(river, { groupId: extraData?.groupId }),
65
+ componentsMapExtraProps: mergeRight(componentsMapExtraProps, { groupId }),
66
+ };
67
+ }, [
68
+ extraProps,
69
+ screenResolverExtraProps,
70
+ river,
71
+ componentsMapExtraProps,
72
+ groupId,
73
+ ]);
56
74
 
57
75
  React.useEffect(() => {
58
76
  if (!isInsideContainer) {
59
- setScreenTitle(stringOrEmpty(screenData?.title));
60
- setScreenSummary(stringOrEmpty(screenData?.summary));
77
+ setScreenTitle(toStringOrEmpty(screenData?.title));
78
+ setScreenSummary(toStringOrEmpty(screenData?.summary));
61
79
  }
62
80
  }, [screenData.id]);
63
81
 
64
82
  React.useEffect(() => {
65
83
  if (feedData && !isInsideContainer) {
66
84
  if (feedData.title && feedData.title !== screenTitle) {
67
- setScreenTitle(stringOrEmpty(feedData.title));
85
+ setScreenTitle(toStringOrEmpty(feedData.title));
68
86
  }
69
87
 
70
88
  if (feedData.summary && feedData.summary !== screenSummary) {
71
- setScreenSummary(stringOrEmpty(feedData.summary));
89
+ setScreenSummary(toStringOrEmpty(feedData.summary));
72
90
  }
73
91
  } else {
74
- setScreenTitle(stringOrEmpty(screenData?.title));
75
- setScreenSummary(stringOrEmpty(screenData?.summary));
92
+ setScreenTitle(toStringOrEmpty(screenData?.title));
93
+ setScreenSummary(toStringOrEmpty(screenData?.summary));
76
94
  }
77
95
  }, [feedData, screenData, screenTitle, screenSummary]);
78
96
 
@@ -86,15 +104,13 @@ export const River = (props: Props) => {
86
104
  }
87
105
 
88
106
  if (river.type !== "general_content") {
89
- const extraData = { ...R.mergeRight(extraProps, screenResolverExtraProps) };
90
-
91
107
  return (
92
108
  <ScreenResolver
93
109
  screenType={river.type}
94
110
  screenId={screenId}
95
- screenData={R.merge(river, { groupId: extraData?.groupId })}
96
- componentsMapExtraProps={componentsMapExtraProps}
97
- {...extraData}
111
+ screenData={screenResolverData.screenData}
112
+ componentsMapExtraProps={screenResolverData.componentsMapExtraProps}
113
+ {...screenResolverData.extraData}
98
114
  />
99
115
  );
100
116
  }
@@ -106,6 +122,7 @@ export const River = (props: Props) => {
106
122
  isScreenWrappedInContainer={isInsideContainer}
107
123
  extraAnchorPointYOffset={extraAnchorPointYOffset}
108
124
  componentsMapExtraProps={componentsMapExtraProps}
125
+ groupId={groupId}
109
126
  />
110
127
  );
111
128
  };