@applicaster/zapp-react-native-ui-components 15.0.0-alpha.9102699023 → 15.0.0-alpha.9102777840

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.
@@ -13,7 +13,7 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
13
13
  import { ScreenTrackedViewPositionsContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenTrackedViewPositionsContext";
14
14
  import { useEventAlerts } from "./utils/useEventAlerts";
15
15
 
16
- const { log_info } = createLogger({
16
+ const { log_debug } = createLogger({
17
17
  category: "ScreenContainer",
18
18
  subsystem: "General",
19
19
  });
@@ -54,20 +54,15 @@ export const GeneralContentScreen = ({
54
54
  useEffect(() => {
55
55
  if (!riverActionProvidersReady) {
56
56
  if (actionsInitialStateSetters.length > 0) {
57
- log_info(
58
- "ScreenContainer: starting to check river action providers to initialize",
59
- { actionsInitialStateSetters }
60
- );
61
-
62
57
  allSettled(actionsInitialStateSetters).finally(() => {
63
- log_info(
58
+ log_debug(
64
59
  "ScreenContainer: action provider ready, completed. Starting to present screen"
65
60
  );
66
61
 
67
62
  setRiverActionProvidersReady(true);
68
63
  });
69
64
  } else {
70
- log_info(
65
+ log_debug(
71
66
  "ScreenContainer: no action provider to check, completed. Starting to present screen"
72
67
  );
73
68
 
@@ -5,8 +5,6 @@ import {
5
5
  usePlugins,
6
6
  } from "@applicaster/zapp-react-native-redux/hooks";
7
7
  import {
8
- useDimensions,
9
- useIsTablet as isTablet,
10
8
  useNavigation,
11
9
  useRivers,
12
10
  } from "@applicaster/zapp-react-native-utils/reactHooks";
@@ -15,8 +13,8 @@ import { BufferAnimation } from "../PlayerContainer/BufferAnimation";
15
13
  import { PlayerContainer } from "../PlayerContainer";
16
14
  import { useModalSize } from "../VideoModal/hooks";
17
15
  import { ViewStyle } from "react-native";
18
- import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
19
16
  import { findCastPlugin, getPlayer } from "./utils";
17
+ import { useWaitForValidOrientation } from "../Screen/hooks";
20
18
 
21
19
  type Props = {
22
20
  item: ZappEntry;
@@ -31,13 +29,6 @@ type PlayableComponent = {
31
29
  Component: React.ComponentType<any>;
32
30
  };
33
31
 
34
- const dimensionsContext: "window" | "screen" = platformSelect({
35
- android_tv: "window",
36
- amazon: "window",
37
- // eslint-disable-next-line react-hooks/rules-of-hooks
38
- default: isTablet() ? "window" : "screen", // on tablet, window represents correct values, on phone it's not as the screen could be rotated
39
- });
40
-
41
32
  export function HandlePlayable({
42
33
  item,
43
34
  isModal,
@@ -97,27 +88,23 @@ export function HandlePlayable({
97
88
  });
98
89
  }, [casting]);
99
90
 
100
- const { width: screenWidth, height: screenHeight } =
101
- useDimensions(dimensionsContext);
102
-
103
91
  const modalSize = useModalSize();
104
92
 
105
- const style = React.useMemo(
106
- () =>
107
- ({
108
- width: isModal
109
- ? modalSize.width
110
- : mode === "PIP"
111
- ? "100%"
112
- : screenWidth,
113
- height: isModal
114
- ? modalSize.height
115
- : mode === "PIP"
116
- ? "100%"
117
- : screenHeight,
118
- }) as ViewStyle,
119
- [screenWidth, screenHeight, modalSize, isModal, mode]
120
- );
93
+ const isOrientationReady = useWaitForValidOrientation();
94
+
95
+ const style = React.useMemo(() => {
96
+ const isFullScreenReady =
97
+ mode === "PIP" || (mode === "FULLSCREEN" && isOrientationReady);
98
+
99
+ const getDimensionValue = (value: string | number) => {
100
+ return isModal ? value : isFullScreenReady ? "100%" : 0; // do not show player, until full screen mode is ready
101
+ };
102
+
103
+ return {
104
+ width: getDimensionValue(modalSize.width),
105
+ height: getDimensionValue(modalSize.height),
106
+ } as ViewStyle;
107
+ }, [modalSize, isModal, mode, isOrientationReady]);
121
108
 
122
109
  const Component = playable?.Component;
123
110
 
@@ -46,7 +46,6 @@ import {
46
46
  PlayerContainerContextProvider,
47
47
  } from "./PlayerContainerContext";
48
48
  import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
49
- import { ErrorDisplay } from "./ErrorDisplay";
50
49
  import { PlayerFocusableWrapperView } from "./WappersView/PlayerFocusableWrapperView";
51
50
  import { FocusableGroupMainContainerId } from "./index";
52
51
  import { isPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypeMatchers";
@@ -61,7 +60,6 @@ import {
61
60
  PlayerNativeSendCommand,
62
61
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
63
62
  import { useAppData } from "@applicaster/zapp-react-native-redux";
64
- import { useRestrictMobilePlayback } from "./useRestrictMobilePlayback";
65
63
 
66
64
  type Props = {
67
65
  Player: React.ComponentType<any>;
@@ -274,12 +272,6 @@ const PlayerContainerComponent = (props: Props) => {
274
272
  );
275
273
  }, [playerManager.isRegistered()]);
276
274
 
277
- const { isRestricted } = useRestrictMobilePlayback({
278
- player,
279
- entry: item,
280
- close,
281
- });
282
-
283
275
  const playEntry = (entry) => navigator.replaceTop(entry, { mode });
284
276
 
285
277
  const onPlayNextPerformNextVideoPlay = React.useCallback(() => {
@@ -346,12 +338,6 @@ const PlayerContainerComponent = (props: Props) => {
346
338
  playerContainerLogger.error(errorObj);
347
339
 
348
340
  setState({ error: errorObj });
349
-
350
- if (!isTvOS) {
351
- setTimeout(() => {
352
- close();
353
- }, 800);
354
- }
355
341
  },
356
342
  [close]
357
343
  );
@@ -669,7 +655,7 @@ const PlayerContainerComponent = (props: Props) => {
669
655
  <PlayerFocusableWrapperView
670
656
  nextFocusDown={context.bottomFocusableId}
671
657
  >
672
- {!Player || isRestricted ? null : (
658
+ {!Player ? null : (
673
659
  <Player
674
660
  source={{
675
661
  uri,
@@ -702,8 +688,6 @@ const PlayerContainerComponent = (props: Props) => {
702
688
  </Player>
703
689
  )}
704
690
  </PlayerFocusableWrapperView>
705
-
706
- {state.error ? <ErrorDisplay error={state.error} /> : null}
707
691
  </View>
708
692
  {/* Components container */}
709
693
  {isInlineTV && context.showComponentsContainer ? (
@@ -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";
@@ -62,9 +64,13 @@ export const usePullToRefresh = (
62
64
 
63
65
  const [refreshing, setRefreshing] = React.useState(false);
64
66
 
65
- const feeds: string[] =
66
- riverComponents?.map(R.path(["data", "source"])).filter((feed) => !!feed) ??
67
- [];
67
+ const feeds: string[] = React.useMemo(
68
+ () =>
69
+ (riverComponents || [])
70
+ .map((riverComponent) => path(["data", "source"], riverComponent))
71
+ .filter((feed) => !isNilOrEmpty(feed)),
72
+ [riverComponents]
73
+ );
68
74
 
69
75
  const feedsLength = feeds.length;
70
76
 
@@ -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) {
@@ -57,7 +57,6 @@ export function ScreenResolverComponent(props: Props) {
57
57
  const { hookPlugin } = screenData || {};
58
58
 
59
59
  const plugins = usePlugins();
60
- const rivers = useRivers();
61
60
 
62
61
  const components = useAppSelector(selectComponents);
63
62
 
@@ -65,12 +64,6 @@ export function ScreenResolverComponent(props: Props) {
65
64
  videoModalState: { mode },
66
65
  } = useNavigation();
67
66
 
68
- const [, setScreenContext] = ZappPipesScreenContext.useZappPipesContext();
69
-
70
- React.useEffect(() => {
71
- setScreenContext(rivers[screenId]);
72
- }, [rivers, screenId, setScreenContext]);
73
-
74
67
  const parentCallback = props.resultCallback;
75
68
 
76
69
  const screenAction = useCallbackActions(
@@ -150,6 +143,17 @@ export function ScreenResolverComponent(props: Props) {
150
143
  return null;
151
144
  }
152
145
 
153
- export const ScreenResolver = ZappPipesScreenContext.withProvider(
154
- ScreenResolverComponent
155
- );
146
+ function withDefaultScreenContext(Component: React.ComponentType<any>) {
147
+ return function WithDefaultScreenContext(props: any) {
148
+ const screenId = props.screenId;
149
+ const rivers = useRivers();
150
+
151
+ return (
152
+ <ZappPipesScreenContext.Provider initialContextValue={rivers[screenId]}>
153
+ <Component {...props} />
154
+ </ZappPipesScreenContext.Provider>
155
+ );
156
+ };
157
+ }
158
+
159
+ export const ScreenResolver = withDefaultScreenContext(ScreenResolverComponent);
@@ -1,9 +1,9 @@
1
- import React, { useEffect } from "react";
1
+ import React from "react";
2
2
  import { equals } from "ramda";
3
3
  import { Animated, ViewProps, ViewStyle } from "react-native";
4
- import { useSafeAreaFrame } from "react-native-safe-area-context";
5
4
 
6
5
  import { useScreenOrientationHandler } from "@applicaster/zapp-react-native-ui-components/Components/Screen/orientationHandler";
6
+ import { useMemoizedSafeAreaFrameWithActiveState } from "@applicaster/zapp-react-native-ui-components/Components/Screen/hooks";
7
7
 
8
8
  import { PathnameContext } from "../../Contexts/PathnameContext";
9
9
  import { ScreenDataContext } from "../../Contexts/ScreenDataContext";
@@ -94,19 +94,13 @@ function SceneComponent({
94
94
  isActive,
95
95
  });
96
96
 
97
- const frame = useSafeAreaFrame();
98
-
99
- const [memoFrame, setMemoFrame] = React.useState(frame);
100
-
101
- useEffect(() => {
102
- if (isActive) {
103
- setMemoFrame((oldFrame) =>
104
- oldFrame.width === frame.width && oldFrame.height === frame.height
105
- ? oldFrame
106
- : frame
107
- );
108
- }
109
- }, [isActive, frame.width, frame.height]);
97
+ // Use shared memoized frame hook - synchronized with useWaitForValidOrientation
98
+ // to prevent race conditions during orientation changes
99
+ // Pass isActive from props since Scene knows its active state from Transitioner
100
+ const memoFrame = useMemoizedSafeAreaFrameWithActiveState({
101
+ updateForInactiveScreens: false,
102
+ isActive,
103
+ });
110
104
 
111
105
  const isAnimating = animating && overlayStyle;
112
106
 
@@ -22,8 +22,6 @@ ReactNative.UIManager.measureLayout = jest.fn(
22
22
  }
23
23
  );
24
24
 
25
- ReactNative.findNodeHandle = () => 1234;
26
-
27
25
  const viewportEventsManager = new ViewportEvents(true);
28
26
 
29
27
  const TestComponent = (props) => {
@@ -88,11 +88,29 @@ const createStore = () =>
88
88
  }))
89
89
  );
90
90
 
91
+ const createScreenComponentsStore = () =>
92
+ create(subscribeWithSelector<Record<string, unknown>>((_) => ({})));
93
+
91
94
  type ScreenContextType = {
92
95
  _navBarStore: ReturnType<typeof createStore>;
93
96
  _stateStore: ReturnType<typeof createStateStore>;
94
97
  navBar: NavBarState;
95
98
  legacyFormatScreenData: LegacyNavigationScreenData | null;
99
+ /**
100
+ * Zustand store for component-level state within a screen.
101
+ *
102
+ * **Purpose:** Persists state across component mount/unmount cycles (e.g., during virtualization)
103
+ * and enables state sharing between components using the same key within the same screen.
104
+ *
105
+ * **Lifecycle:** Tied to the screen/route — recreated on each route change.
106
+ *
107
+ * @example
108
+ * // Used by useComponentScreenState hook:
109
+ * const store = useScreenContextV2()._componentStateStore;
110
+ * store.setState({ 'my-key': value });
111
+ * const value = store.getState()['my-key'];
112
+ */
113
+ _componentStateStore: ReturnType<typeof createScreenComponentsStore>;
96
114
  };
97
115
 
98
116
  export const ScreenContext = createContext<ScreenContextType>({
@@ -107,6 +125,7 @@ export const ScreenContext = createContext<ScreenContextType>({
107
125
  setSummary: (_subtitle) => void 0,
108
126
  },
109
127
  legacyFormatScreenData: {} as LegacyNavigationScreenData,
128
+ _componentStateStore: createScreenComponentsStore(),
110
129
  });
111
130
 
112
131
  export function ScreenContextProvider({
@@ -160,6 +179,14 @@ export function ScreenContextProvider({
160
179
  return navBarState;
161
180
  }, []);
162
181
 
182
+ // Component state store - recreated when pathname changes (route change).
183
+ // Unlike _navBarStore and _stateStore (cached via refs), this store
184
+ // resets only when pathname changes to provide a clean state for the new route.
185
+ const componentStateStore = useMemo(
186
+ () => createScreenComponentsStore(),
187
+ [pathname]
188
+ );
189
+
163
190
  const screenNavBarState = getScreenNavBarState()(
164
191
  useShallow((state) => ({
165
192
  visible: state.visible,
@@ -212,8 +239,9 @@ export function ScreenContextProvider({
212
239
  _stateStore: getScreenState(),
213
240
  navBar: navBarState,
214
241
  legacyFormatScreenData: routeScreenData,
242
+ _componentStateStore: componentStateStore,
215
243
  }),
216
- [navBarState, screenData, routeScreenData]
244
+ [navBarState, screenData, routeScreenData, componentStateStore]
217
245
  )}
218
246
  >
219
247
  {children}
@@ -5,10 +5,10 @@ import React, {
5
5
  useMemo,
6
6
  useState,
7
7
  } from "react";
8
- import * as R from "ramda";
9
8
 
10
- type ProviderProps = {
9
+ type ProviderProps<S> = {
11
10
  children: React.ReactChild;
11
+ initialContextValue?: S;
12
12
  };
13
13
 
14
14
  type ContextType<T> = {
@@ -27,21 +27,29 @@ export function createZappPipesContext<T, S = T>(
27
27
  ) {
28
28
  const Context = createContext<ContextType<S>>(initialContext);
29
29
 
30
- const { selector = R.identity, prepareContext = R.identity } = options || {};
30
+ const defaultSelector = (c: any) => c;
31
+ const defaultPrepareContext = (n: any) => n;
32
+ const joinArgs = (args: any[]) => args.join("-");
33
+
34
+ const { selector = defaultSelector, prepareContext = defaultPrepareContext } =
35
+ options || {};
31
36
 
32
37
  function useZappPipesContext(...hookArgs: any[]): [T, (T) => void] {
33
38
  const { context, setContext } = useContext(Context);
39
+ const joinedArgs = joinArgs(hookArgs);
34
40
 
35
41
  const contextValue = useMemo(
36
42
  () => selector(context, ...hookArgs),
37
- [context, R.join("-", hookArgs)]
43
+ // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
44
+ [context, joinedArgs]
38
45
  );
39
46
 
40
47
  const contextSetter = useCallback(
41
48
  (newContext: T) => {
42
49
  setContext(prepareContext(newContext, context, ...hookArgs));
43
50
  },
44
- [context, setContext, R.join("-", hookArgs)]
51
+ // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
52
+ [context, joinedArgs]
45
53
  );
46
54
 
47
55
  return useMemo(
@@ -50,8 +58,11 @@ export function createZappPipesContext<T, S = T>(
50
58
  );
51
59
  }
52
60
 
53
- function Provider({ children }: ProviderProps) {
54
- const [context, setContext] = useState<S>(initialContext.context);
61
+ /** Provider accepts `initialContextValue` prop to set the initial context value */
62
+ function Provider({ children, initialContextValue }: ProviderProps<S>) {
63
+ const [context, setContext] = useState<S>(
64
+ initialContextValue ?? initialContext.context
65
+ );
55
66
 
56
67
  return (
57
68
  <Context.Provider value={{ context, setContext }}>
@@ -1,4 +1,3 @@
1
- // ResolverSelector.tsx
2
1
  import React from "react";
3
2
  import { ComponentDataSourceContext, ZappPipesDataProps } from "./types";
4
3
  import { StaticFeedResolver } from "./resolvers/StaticFeedResolver";
@@ -12,14 +11,33 @@ type ResolverSelectorProps = ComponentDataSourceContext & {
12
11
  export function ResolverSelector(props: ResolverSelectorProps) {
13
12
  const { getStaticComponentFeed, component, feedUrl, children } = props;
14
13
 
15
- // Determine which resolver to use
14
+ const renderDefaultResolver = (
15
+ fallbackChildren: (dataProps: ZappPipesDataProps) => React.ReactNode
16
+ ) => {
17
+ if (feedUrl || component?.data?.source) {
18
+ return <UrlFeedResolver {...props}>{fallbackChildren}</UrlFeedResolver>;
19
+ }
20
+
21
+ return <NullFeedResolver>{fallbackChildren}</NullFeedResolver>;
22
+ };
23
+
16
24
  if (getStaticComponentFeed) {
17
- return <StaticFeedResolver {...props}>{children}</StaticFeedResolver>;
18
- }
25
+ return (
26
+ <StaticFeedResolver {...props}>
27
+ {(zappPipesDataProps) => {
28
+ const { data, loading, error } = zappPipesDataProps.zappPipesData;
29
+
30
+ const shouldFallback = !loading && data === null && !error;
31
+
32
+ if (shouldFallback) {
33
+ return renderDefaultResolver(children);
34
+ }
19
35
 
20
- if (feedUrl || component?.data?.source) {
21
- return <UrlFeedResolver {...props}>{children}</UrlFeedResolver>;
36
+ return children(zappPipesDataProps);
37
+ }}
38
+ </StaticFeedResolver>
39
+ );
22
40
  }
23
41
 
24
- return <NullFeedResolver>{children}</NullFeedResolver>;
42
+ return renderDefaultResolver(children);
25
43
  }
@@ -7,15 +7,30 @@ import { UrlFeedResolver } from "../resolvers/UrlFeedResolver";
7
7
  import { NullFeedResolver } from "../resolvers/NullFeedResolver";
8
8
 
9
9
  jest.mock("../resolvers/StaticFeedResolver", () => ({
10
- StaticFeedResolver: jest.fn(() => null),
10
+ StaticFeedResolver: jest.fn(({ children }) =>
11
+ children({
12
+ zappPipesData: { loading: true, data: null, error: null },
13
+ reloadData: jest.fn(),
14
+ })
15
+ ),
11
16
  }));
12
17
 
13
18
  jest.mock("../resolvers/UrlFeedResolver", () => ({
14
- UrlFeedResolver: jest.fn(() => null),
19
+ UrlFeedResolver: jest.fn(({ children }) =>
20
+ children({
21
+ zappPipesData: { loading: true, data: null, error: null },
22
+ reloadData: jest.fn(),
23
+ })
24
+ ),
15
25
  }));
16
26
 
17
27
  jest.mock("../resolvers/NullFeedResolver", () => ({
18
- NullFeedResolver: jest.fn(() => null),
28
+ NullFeedResolver: jest.fn(({ children }) =>
29
+ children({
30
+ zappPipesData: { loading: false, data: null, error: null },
31
+ reloadData: jest.fn(),
32
+ })
33
+ ),
19
34
  }));
20
35
 
21
36
  const testPlugin = {
@@ -52,7 +67,7 @@ describe("ResolverSelector", () => {
52
67
  expect.objectContaining({
53
68
  getStaticComponentFeed: props.getStaticComponentFeed,
54
69
  component: mockComponent,
55
- children: mockChildren,
70
+ children: expect.any(Function),
56
71
  }),
57
72
  expect.anything()
58
73
  );
@@ -151,7 +166,10 @@ describe("ResolverSelector", () => {
151
166
  render(<ResolverSelector {...props} />);
152
167
 
153
168
  expect(StaticFeedResolver).toHaveBeenCalledWith(
154
- expect.objectContaining(props),
169
+ expect.objectContaining({
170
+ ...props,
171
+ children: expect.any(Function),
172
+ }),
155
173
  expect.anything()
156
174
  );
157
175
  });
@@ -202,4 +220,193 @@ describe("ResolverSelector", () => {
202
220
  expect(UrlFeedResolver).not.toHaveBeenCalled();
203
221
  expect(NullFeedResolver).not.toHaveBeenCalled();
204
222
  });
223
+
224
+ describe("Fallback Logic", () => {
225
+ it("should fallback to UrlFeedResolver when StaticFeedResolver returns null and feedUrl is present", () => {
226
+ const props = {
227
+ getStaticComponentFeed: jest.fn(),
228
+ feedUrl: "https://example.com/feed",
229
+ component: mockComponent,
230
+ children: mockChildren,
231
+ riverId: "test-river",
232
+ };
233
+
234
+ (StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
235
+ children({
236
+ zappPipesData: { loading: false, data: null, error: null },
237
+ reloadData: jest.fn(),
238
+ })
239
+ );
240
+
241
+ render(<ResolverSelector {...props} />);
242
+
243
+ expect(StaticFeedResolver).toHaveBeenCalled();
244
+
245
+ expect(UrlFeedResolver).toHaveBeenCalledWith(
246
+ expect.objectContaining({
247
+ feedUrl: props.feedUrl,
248
+ component: mockComponent,
249
+ children: mockChildren,
250
+ }),
251
+ expect.anything()
252
+ );
253
+ });
254
+
255
+ it("should fallback to UrlFeedResolver when StaticFeedResolver returns null and component.data.source is present", () => {
256
+ const componentWithSource = {
257
+ ...mockComponent,
258
+ data: {
259
+ source: "data-source",
260
+ },
261
+ };
262
+
263
+ const props = {
264
+ getStaticComponentFeed: jest.fn(),
265
+ component: componentWithSource,
266
+ children: mockChildren,
267
+ riverId: "test-river",
268
+ };
269
+
270
+ (StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
271
+ children({
272
+ zappPipesData: { loading: false, data: null, error: null },
273
+ reloadData: jest.fn(),
274
+ })
275
+ );
276
+
277
+ render(<ResolverSelector {...props} />);
278
+
279
+ expect(StaticFeedResolver).toHaveBeenCalled();
280
+
281
+ expect(UrlFeedResolver).toHaveBeenCalledWith(
282
+ expect.objectContaining({
283
+ component: componentWithSource,
284
+ children: mockChildren,
285
+ }),
286
+ expect.anything()
287
+ );
288
+ });
289
+
290
+ it("should fallback to NullFeedResolver when StaticFeedResolver returns null and no feedUrl/source is present", () => {
291
+ const props = {
292
+ getStaticComponentFeed: jest.fn(),
293
+ component: mockComponent,
294
+ children: mockChildren,
295
+ riverId: "test-river",
296
+ };
297
+
298
+ (StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
299
+ children({
300
+ zappPipesData: { loading: false, data: null, error: null },
301
+ reloadData: jest.fn(),
302
+ })
303
+ );
304
+
305
+ render(<ResolverSelector {...props} />);
306
+
307
+ expect(StaticFeedResolver).toHaveBeenCalled();
308
+ expect(UrlFeedResolver).not.toHaveBeenCalled();
309
+
310
+ expect(NullFeedResolver).toHaveBeenCalledWith(
311
+ expect.objectContaining({
312
+ children: mockChildren,
313
+ }),
314
+ expect.anything()
315
+ );
316
+ });
317
+
318
+ it("should NOT fallback and call children with static data when StaticFeedResolver returns data", () => {
319
+ const staticData = { entry: [{ id: "1" }] };
320
+
321
+ const props = {
322
+ getStaticComponentFeed: jest.fn(),
323
+ feedUrl: "https://example.com/feed",
324
+ component: mockComponent,
325
+ children: mockChildren,
326
+ riverId: "test-river",
327
+ };
328
+
329
+ (StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
330
+ children({
331
+ zappPipesData: { loading: false, data: staticData, error: null },
332
+ reloadData: jest.fn(),
333
+ })
334
+ );
335
+
336
+ render(<ResolverSelector {...props} />);
337
+
338
+ expect(StaticFeedResolver).toHaveBeenCalled();
339
+ expect(UrlFeedResolver).not.toHaveBeenCalled();
340
+
341
+ expect(mockChildren).toHaveBeenCalledWith(
342
+ expect.objectContaining({
343
+ zappPipesData: expect.objectContaining({
344
+ data: staticData,
345
+ }),
346
+ })
347
+ );
348
+ });
349
+
350
+ it("should NOT fallback and call children with loading state when StaticFeedResolver is loading", () => {
351
+ const props = {
352
+ getStaticComponentFeed: jest.fn(),
353
+ feedUrl: "https://example.com/feed",
354
+ component: mockComponent,
355
+ children: mockChildren,
356
+ riverId: "test-river",
357
+ };
358
+
359
+ (StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
360
+ children({
361
+ zappPipesData: { loading: true, data: null, error: null },
362
+ reloadData: jest.fn(),
363
+ })
364
+ );
365
+
366
+ render(<ResolverSelector {...props} />);
367
+
368
+ expect(StaticFeedResolver).toHaveBeenCalled();
369
+ expect(UrlFeedResolver).not.toHaveBeenCalled();
370
+
371
+ expect(mockChildren).toHaveBeenCalledWith(
372
+ expect.objectContaining({
373
+ zappPipesData: expect.objectContaining({
374
+ loading: true,
375
+ }),
376
+ })
377
+ );
378
+ });
379
+
380
+ it("should NOT fallback and call children with error when StaticFeedResolver returns error", () => {
381
+ const error = new Error("Static error");
382
+
383
+ const props = {
384
+ getStaticComponentFeed: jest.fn(),
385
+ feedUrl: "https://example.com/feed",
386
+ component: mockComponent,
387
+ children: mockChildren,
388
+ riverId: "test-river",
389
+ };
390
+
391
+ (StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
392
+ children({
393
+ zappPipesData: { loading: false, data: null, error },
394
+ reloadData: jest.fn(),
395
+ })
396
+ );
397
+
398
+ render(<ResolverSelector {...props} />);
399
+
400
+ expect(StaticFeedResolver).toHaveBeenCalled();
401
+ expect(UrlFeedResolver).not.toHaveBeenCalled();
402
+
403
+ expect(mockChildren).toHaveBeenCalledWith(
404
+ expect.objectContaining({
405
+ zappPipesData: expect.objectContaining({
406
+ error,
407
+ }),
408
+ })
409
+ );
410
+ });
411
+ });
205
412
  });
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
2
  import * as useFeedLoaderModule from "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedLoader";
3
- import * as useFeedRefreshModule from "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedRefresh";
4
3
  import { favoritesListener } from "@applicaster/zapp-react-native-bridge/Favorites";
5
4
  import { UrlFeedResolver } from "../resolvers/UrlFeedResolver";
6
5
  import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
@@ -14,10 +13,6 @@ jest
14
13
 
15
14
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedLoader");
16
15
 
17
- jest.mock(
18
- "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedRefresh"
19
- );
20
-
21
16
  jest.mock("@applicaster/zapp-pipes-v2-client");
22
17
 
23
18
  jest.mock("@applicaster/zapp-react-native-bridge/Favorites", () => ({
@@ -62,6 +57,9 @@ describe("UrlFeedResolver", () => {
62
57
  component: { ...componentRequiredKeys } as any,
63
58
  children: mockChildren,
64
59
  riverId: "test-river",
60
+ screenContext: {
61
+ id: "screenId",
62
+ },
65
63
  };
66
64
 
67
65
  renderWithProviders(<UrlFeedResolver {...props} />);
@@ -82,6 +80,9 @@ describe("UrlFeedResolver", () => {
82
80
  } as any,
83
81
  children: mockChildren,
84
82
  riverId: "test-river",
83
+ screenContext: {
84
+ id: "screenId",
85
+ },
85
86
  };
86
87
 
87
88
  renderWithProviders(<UrlFeedResolver {...props} />);
@@ -103,6 +104,9 @@ describe("UrlFeedResolver", () => {
103
104
  } as any,
104
105
  children: mockChildren,
105
106
  riverId: "test-river",
107
+ screenContext: {
108
+ id: "screenId",
109
+ },
106
110
  };
107
111
 
108
112
  renderWithProviders(<UrlFeedResolver {...props} />);
@@ -132,6 +136,9 @@ describe("UrlFeedResolver", () => {
132
136
  } as any,
133
137
  children: mockChildren,
134
138
  riverId: "test-river",
139
+ screenContext: {
140
+ id: "screenId",
141
+ },
135
142
  };
136
143
 
137
144
  renderWithProviders(<UrlFeedResolver {...props} />);
@@ -155,6 +162,9 @@ describe("UrlFeedResolver", () => {
155
162
  children: mockChildren,
156
163
  riverId: "test-river",
157
164
  plugins: [],
165
+ screenContext: {
166
+ id: "screenId",
167
+ },
158
168
  };
159
169
 
160
170
  renderWithProviders(<UrlFeedResolver {...props} />);
@@ -173,6 +183,9 @@ describe("UrlFeedResolver", () => {
173
183
  component: { ...componentRequiredKeys } as any,
174
184
  children: mockChildren,
175
185
  riverId: "test-river",
186
+ screenContext: {
187
+ id: "screenId",
188
+ },
176
189
  };
177
190
 
178
191
  renderWithProviders(<UrlFeedResolver {...props} />);
@@ -197,6 +210,9 @@ describe("UrlFeedResolver", () => {
197
210
  },
198
211
  },
199
212
  ] as any,
213
+ screenContext: {
214
+ id: "screenId",
215
+ },
200
216
  };
201
217
 
202
218
  renderWithProviders(<UrlFeedResolver {...props} />);
@@ -215,6 +231,9 @@ describe("UrlFeedResolver", () => {
215
231
  } as any,
216
232
  children: mockChildren,
217
233
  riverId: "test-river",
234
+ screenContext: {
235
+ id: "screenId",
236
+ },
218
237
  };
219
238
 
220
239
  (useFeedLoaderModule.useFeedLoader as jest.Mock).mockReturnValue({
@@ -250,6 +269,9 @@ describe("UrlFeedResolver", () => {
250
269
  children: mockChildren,
251
270
  riverId: "test-river",
252
271
  isLast: true,
272
+ screenContext: {
273
+ id: "screenId",
274
+ },
253
275
  };
254
276
 
255
277
  renderWithProviders(<UrlFeedResolver {...props1} />);
@@ -272,6 +294,9 @@ describe("UrlFeedResolver", () => {
272
294
  children: mockChildren,
273
295
  riverId: "test-river",
274
296
  isLast: false,
297
+ screenContext: {
298
+ id: "screenId",
299
+ },
275
300
  };
276
301
 
277
302
  renderWithProviders(<UrlFeedResolver {...props2} />);
@@ -294,6 +319,9 @@ describe("UrlFeedResolver", () => {
294
319
  children: mockChildren,
295
320
  riverId: "test-river",
296
321
  isLast: false,
322
+ screenContext: {
323
+ id: "screenId",
324
+ },
297
325
  };
298
326
 
299
327
  renderWithProviders(<UrlFeedResolver {...props3} />);
@@ -311,6 +339,9 @@ describe("UrlFeedResolver", () => {
311
339
  component: { ...componentRequiredKeys } as any,
312
340
  children: mockChildren,
313
341
  riverId: "test-river",
342
+ screenContext: {
343
+ id: "screenId",
344
+ },
314
345
  };
315
346
 
316
347
  renderWithProviders(<UrlFeedResolver {...props} />);
@@ -327,22 +358,6 @@ describe("UrlFeedResolver", () => {
327
358
  });
328
359
  });
329
360
 
330
- it("should apply feed refresh hook", () => {
331
- const props = {
332
- feedUrl: "https://example.com/feed",
333
- component: { ...componentRequiredKeys } as any,
334
- children: mockChildren,
335
- riverId: "test-river",
336
- };
337
-
338
- renderWithProviders(<UrlFeedResolver {...props} />);
339
-
340
- expect(useFeedRefreshModule.useFeedRefresh).toHaveBeenCalledWith({
341
- reloadData: mockReloadData,
342
- component: props.component,
343
- });
344
- });
345
-
346
361
  it("should clean up listeners on unmount", () => {
347
362
  const props = {
348
363
  component: {
@@ -354,6 +369,9 @@ describe("UrlFeedResolver", () => {
354
369
  } as any,
355
370
  children: mockChildren,
356
371
  riverId: "test-river",
372
+ screenContext: {
373
+ id: "screenId",
374
+ },
357
375
  };
358
376
 
359
377
  const { unmount } = renderWithProviders(<UrlFeedResolver {...props} />);
@@ -9,9 +9,9 @@ import {
9
9
  getInflatedDataSourceUrl,
10
10
  getSearchContext,
11
11
  useFeedLoader,
12
- useFeedRefresh,
13
12
  useRoute,
14
13
  } from "@applicaster/zapp-react-native-utils/reactHooks";
14
+ import { refreshCoordinator } from "@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator";
15
15
 
16
16
  import { ComponentDataSourceContext, ZappPipesDataProps } from "../types";
17
17
  import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
@@ -199,6 +199,8 @@ export function UrlFeedResolver({
199
199
  const { pathname } = useRoute();
200
200
  const screenStateStore = useScreenStateStore();
201
201
 
202
+ const screenId = (screenContext?.id || "unknown_screen_id") as string;
203
+
202
204
  // Setup listeners for data source URL
203
205
  useEffect(() => {
204
206
  if (!reloadData) {
@@ -214,10 +216,19 @@ export function UrlFeedResolver({
214
216
  url: dataSourceUrl,
215
217
  pathname,
216
218
  screenStateStore,
219
+ component,
220
+ screenId,
217
221
  callback: reloadData,
218
222
  });
219
223
  }
220
- }, [dataSourceUrl, reloadData, pathname, screenStateStore]);
224
+ }, [
225
+ dataSourceUrl,
226
+ reloadData,
227
+ pathname,
228
+ screenStateStore,
229
+ component,
230
+ screenId,
231
+ ]);
221
232
 
222
233
  // Setup favorites listener
223
234
  useEffect(() => {
@@ -230,11 +241,11 @@ export function UrlFeedResolver({
230
241
  }
231
242
  }, [type, reloadData]);
232
243
 
233
- // Apply feed refresh hook
234
- useFeedRefresh({
235
- reloadData,
236
- component,
237
- });
244
+ useEffect(() => {
245
+ const unregister = refreshCoordinator.register(component, screenId);
246
+
247
+ return () => unregister();
248
+ }, [component, screenId]);
238
249
 
239
250
  const loadNextData = useMemo(
240
251
  () => (!isLast && isVerticalListOrGrid(component) ? undefined : loadNext),
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.9102699023",
3
+ "version": "15.0.0-alpha.9102777840",
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.9102699023",
32
- "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.9102699023",
33
- "@applicaster/zapp-react-native-redux": "15.0.0-alpha.9102699023",
34
- "@applicaster/zapp-react-native-utils": "15.0.0-alpha.9102699023",
31
+ "@applicaster/applicaster-types": "15.0.0-alpha.9102777840",
32
+ "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.9102777840",
33
+ "@applicaster/zapp-react-native-redux": "15.0.0-alpha.9102777840",
34
+ "@applicaster/zapp-react-native-utils": "15.0.0-alpha.9102777840",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",
@@ -1,57 +0,0 @@
1
- import * as React from "react";
2
- import * as R from "ramda";
3
- import { Text, TextStyle, View, ViewStyle } from "react-native";
4
-
5
- import { getLocalizations } from "@applicaster/zapp-react-native-utils/localizationUtils";
6
- import { getAppStylesColor } from "@applicaster/zapp-react-native-utils/stylesUtils";
7
- import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
8
- import { styleKeys } from "@applicaster/zapp-react-native-utils/styleKeysUtils";
9
-
10
- type Props = {
11
- styles: {};
12
- error: {};
13
- remoteConfigurations: { localizations: {} };
14
- };
15
-
16
- const defaultAppStyles = {
17
- loading_error_label: {
18
- color: "#aaa",
19
- },
20
- };
21
-
22
- const textStyles = (appStyles = defaultAppStyles): TextStyle => ({
23
- color: getAppStylesColor("loading_error_label", appStyles),
24
- fontSize: 36,
25
- textAlign: "center",
26
- });
27
-
28
- const errorStyles = ({ backgroundColor }): ViewStyle => ({
29
- flex: 1,
30
- width: "100%",
31
- height: "100%",
32
- justifyContent: "center",
33
- alignItems: "center",
34
- position: "absolute",
35
- zIndex: 100,
36
- backgroundColor,
37
- });
38
-
39
- export function ErrorDisplayComponent({
40
- styles,
41
- remoteConfigurations: { localizations },
42
- }: Props) {
43
- const theme = useTheme();
44
- const backgroundColor = theme?.app_background_color;
45
-
46
- const { stream_error_message = "Cannot play stream" } = getLocalizations({
47
- localizations,
48
- });
49
-
50
- const appStyles = R.prop(styleKeys.style_namespace, styles);
51
-
52
- return (
53
- <View style={errorStyles({ backgroundColor })}>
54
- <Text style={textStyles(appStyles)}>{stream_error_message}</Text>
55
- </View>
56
- );
57
- }
@@ -1,9 +0,0 @@
1
- import * as R from "ramda";
2
-
3
- import { connectToStore } from "@applicaster/zapp-react-native-redux/utils/connectToStore";
4
-
5
- import { ErrorDisplayComponent } from "./ErrorDisplay";
6
-
7
- export const ErrorDisplay = R.compose(
8
- connectToStore(R.pick(["remoteConfigurations"]))
9
- )(ErrorDisplayComponent);
@@ -1,131 +0,0 @@
1
- import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
2
- import NetInfo from "@react-native-community/netinfo";
3
-
4
- import { useCallback, 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 { 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";
11
- import { log_info } from "./logger";
12
- import { isTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
13
-
14
- type RestrictMobilePlaybackProps = {
15
- player?: Player;
16
- entry?: ZappEntry;
17
- close: () => void;
18
- };
19
-
20
- export const useRestrictMobilePlayback = ({
21
- player,
22
- entry,
23
- close,
24
- }: RestrictMobilePlaybackProps): { isRestricted: boolean } => {
25
- const dialogVisibleRef = useRef<boolean>(false);
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
- );
48
-
49
- useEffect(() => {
50
- return () => {
51
- if (isTV()) {
52
- return;
53
- }
54
-
55
- dialogVisibleRef.current = false;
56
- };
57
- }, []);
58
-
59
- const isConnectionRestricted = useMemo(() => {
60
- if (isTV()) {
61
- return false;
62
- }
63
-
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]);
71
-
72
- const [isRestricted, setIsRestricted] = useState<boolean>(
73
- isConnectionRestricted
74
- );
75
-
76
- useEffect(() => {
77
- if (!isConnectionRestricted) {
78
- return;
79
- }
80
-
81
- const stopPlayer = () => {
82
- log_info(
83
- "Stopping player due to mobile restriction, connection_restricted: true"
84
- );
85
-
86
- player?.closeNativePlayer();
87
- playerManager?.invokeHandler("close");
88
-
89
- dialogVisibleRef.current = true;
90
-
91
- showAlertDialog({
92
- title:
93
- localize("restrict_mobile_playback_error_title") ||
94
- "Restricted Connection Type",
95
- message:
96
- localize("restrict_mobile_playback_error_message") ||
97
- "This content can only be viewed over a Wi-Fi or LAN network.",
98
- okButtonText: theme.ok_button || "OK",
99
- completion: () => {
100
- dialogVisibleRef.current = false;
101
-
102
- close();
103
- },
104
- });
105
- };
106
-
107
- return NetInfo.addEventListener((state) => {
108
- if (state.type === "cellular") {
109
- setIsRestricted(true);
110
-
111
- if (dialogVisibleRef.current) {
112
- return;
113
- }
114
-
115
- stopPlayer();
116
- } else {
117
- setIsRestricted(false);
118
- }
119
- });
120
- }, [
121
- close,
122
- entry?.extensions?.connection_restricted,
123
- localizations,
124
- localize,
125
- player,
126
- theme.ok_button,
127
- isConnectionRestricted,
128
- ]);
129
-
130
- return { isRestricted };
131
- };