@applicaster/zapp-react-native-ui-components 16.0.0-rc.5 → 16.0.0-rc.51

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 (105) hide show
  1. package/Components/BackgroundImage/BackgroundImage.android.tv.tsx +28 -0
  2. package/Components/BackgroundImage/BackgroundImage.ios.tv.tsx +24 -0
  3. package/Components/BackgroundImage/BackgroundImage.tsx +42 -0
  4. package/Components/BackgroundImage/index.ts +1 -0
  5. package/Components/CellRendererResolver/index.ts +2 -2
  6. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  7. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  8. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
  9. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  10. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  11. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +124 -0
  14. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  15. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  16. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  17. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +72 -0
  18. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  19. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  20. package/Components/GeneralContentScreen/index.ts +2 -0
  21. package/Components/Layout/FullWidthRow.tsx +38 -0
  22. package/Components/Layout/TV/ScreenContainer.tsx +5 -0
  23. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +0 -1
  24. package/Components/Layout/TV/index.tsx +3 -4
  25. package/Components/Layout/TV/index.web.tsx +2 -3
  26. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +16 -5
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  29. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +1 -1
  30. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  31. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  32. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  33. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  34. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  35. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  36. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  37. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  38. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  46. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  47. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  48. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  49. package/Components/MasterCell/dataAdapter.ts +15 -5
  50. package/Components/MasterCell/hooks/index.ts +2 -0
  51. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  52. package/Components/MasterCell/index.tsx +30 -5
  53. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  54. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  55. package/Components/MasterCell/utils/index.ts +85 -46
  56. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +332 -0
  57. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +402 -0
  58. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +811 -0
  59. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +671 -0
  60. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +878 -0
  61. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  62. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  63. package/Components/ModalComponent/BottomSheetModalContent.tsx +49 -26
  64. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  65. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  66. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  67. package/Components/OfflineHandler/hooks.ts +123 -0
  68. package/Components/OfflineHandler/index.tsx +51 -73
  69. package/Components/OfflineHandler/utils/index.ts +1 -13
  70. package/Components/PlayerContainer/PlayerContainer.tsx +23 -13
  71. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +285 -0
  72. package/Components/PreloaderWrapper/index.tsx +1 -1
  73. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
  74. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
  75. package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
  76. package/Components/Screen/TV/hooks/index.ts +2 -0
  77. package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
  78. package/Components/Screen/TV/index.web.tsx +16 -7
  79. package/Components/Screen/index.tsx +8 -4
  80. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  81. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  82. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  83. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  84. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +8 -19
  85. package/Components/TopCutoffOverlay/index.tsx +2 -2
  86. package/Components/Transitioner/Transitioner.tsx +28 -17
  87. package/Components/VideoLive/LiveImageManager.ts +56 -45
  88. package/Components/VideoLive/PlayerLiveImageComponent.tsx +4 -2
  89. package/Components/VideoModal/utils.ts +6 -1
  90. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  91. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  92. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  93. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  94. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  95. package/Helpers/index.js +7 -40
  96. package/package.json +5 -5
  97. package/Components/Layout/TV/LayoutBackground.tsx +0 -31
  98. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  99. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  100. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  101. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  102. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  103. package/Components/Screen/utils.ts +0 -16
  104. package/Helpers/Analytics/index.js +0 -95
  105. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
@@ -0,0 +1,9 @@
1
+ export { AudioPlayerAction } from "./Action";
2
+
3
+ export { AudioPlayerButton } from "./Button";
4
+
5
+ export { AudioPlayerHeader } from "./Header";
6
+
7
+ export { AudioPlayerTrackItem } from "./Item";
8
+
9
+ export { AudioPlayerInput } from "./Input";
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { PanGestureHandler } from "react-native-gesture-handler";
3
+ import { View } from "react-native";
4
+ import { useBottomSheet } from "@applicaster/zapp-react-native-utils/bottomSheet";
5
+
6
+ /**
7
+ * Turns its children into a drag zone for the enclosing DraggableBottomSheet:
8
+ * dragging here moves/dismisses the sheet (used for the modal header bar).
9
+ * Outside a sheet (no context) it renders children unchanged.
10
+ */
11
+ export const BottomSheetDragArea = ({
12
+ children,
13
+ }: {
14
+ children: React.ReactElement;
15
+ }) => {
16
+ const sheet = useBottomSheet();
17
+
18
+ // Outside a sheet (no context) render children unchanged.
19
+ if (!sheet) return children;
20
+
21
+ return (
22
+ <PanGestureHandler
23
+ simultaneousHandlers={[sheet.masterDrawerRef]}
24
+ shouldCancelWhenOutside={false}
25
+ onGestureEvent={sheet.onDragGestureEvent}
26
+ onHandlerStateChange={sheet.onDragStateChange}
27
+ >
28
+ {/* Old-API PanGestureHandler needs a host-component child; children may
29
+ be a function component (e.g. ModalHeader), so wrap in a View. */}
30
+ <View>{children}</View>
31
+ </PanGestureHandler>
32
+ );
33
+ };
@@ -6,11 +6,11 @@ import { ItemProps } from "./Button/Item";
6
6
  import { ItemLabelProps } from "./Button/ItemLabel";
7
7
  import { getItemIconProps, getItemLabelProps, getItemProps } from "./utils";
8
8
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
9
-
10
9
  import type { PluginConfiguration } from "./";
11
10
 
12
11
  import { ModalHeader as DefaultModalHeader } from "./Header";
13
12
  import { useSafeAreaInsets } from "react-native-safe-area-context";
13
+ import { BottomSheetDragArea } from "./BottomSheetDragArea";
14
14
 
15
15
  type ModalComponentProps = {
16
16
  items: any[];
@@ -23,6 +23,12 @@ type ModalComponentProps = {
23
23
  maxHeight?: number;
24
24
  dismiss: () => void;
25
25
  headerComponent?: React.ComponentType;
26
+
27
+ scrollViewContent?: {
28
+ renderItem: (item: any, index: number) => React.ReactNode;
29
+ items: any[];
30
+ } | null;
31
+
26
32
  buttonComponent?: React.ComponentType;
27
33
  iconProps?: ItemIconProps | ((theme: PluginConfiguration) => ItemIconProps);
28
34
  itemProps?:
@@ -49,7 +55,9 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
49
55
  dismiss,
50
56
  summary,
51
57
  title,
52
- headerComponent: ModalHeader = DefaultModalHeader,
58
+ headerComponent = DefaultModalHeader,
59
+ scrollViewContent = null,
60
+
53
61
  buttonComponent: ButtonComponent = Button,
54
62
  getSelectedItemIcon = ({
55
63
  modal_bottom_sheet_item_selected_icon,
@@ -63,6 +71,8 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
63
71
  const paddingTop = Number(theme.modal_bottom_sheet_padding_top);
64
72
  const paddingBottom = Number(theme.modal_bottom_sheet_padding_bottom);
65
73
 
74
+ const Header = headerComponent;
75
+
66
76
  useEffect(() => {
67
77
  if (currentRoute !== route.current) {
68
78
  props.dismiss();
@@ -98,35 +108,48 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
98
108
  paddingTop: paddingTop,
99
109
  }}
100
110
  >
101
- <ModalHeader
102
- dismiss={dismiss}
103
- configuration={theme}
104
- summary={summary}
105
- title={title}
106
- />
111
+ {Header ? (
112
+ <BottomSheetDragArea>
113
+ <Header
114
+ dismiss={dismiss}
115
+ configuration={theme}
116
+ summary={summary}
117
+ title={title}
118
+ />
119
+ </BottomSheetDragArea>
120
+ ) : null}
107
121
  <ScrollView
122
+ overScrollMode="never"
123
+ bounces={false}
124
+ showsVerticalScrollIndicator={false}
108
125
  contentContainerStyle={{
109
126
  paddingBottom: paddingBottom + bottomInset,
110
127
  }}
111
128
  >
112
- {items.map((item, index) =>
113
- item ? (
114
- <ButtonComponent
115
- key={index}
116
- configuration={theme}
117
- selectedItem={current_selection}
118
- item={item}
119
- onPress={handlePress}
120
- label={theme[item?.label] ?? item?.label}
121
- iconBaseProps={iconBaseProps}
122
- itemBaseProps={itemBaseProps}
123
- labelBaseProps={labelBaseProps}
124
- selectedItemIcon={getSelectedItemIcon(theme)}
125
- defaultItemIcon={getDefaultItemIcon(theme)}
126
- iconPlacement={iconPlacement}
127
- />
128
- ) : null
129
- )}
129
+ {scrollViewContent
130
+ ? scrollViewContent.items.map((item, index) =>
131
+ scrollViewContent.renderItem(item, index)
132
+ )
133
+ : null}
134
+ {!scrollViewContent &&
135
+ items.map((item, index) =>
136
+ item ? (
137
+ <ButtonComponent
138
+ key={index}
139
+ configuration={theme}
140
+ selectedItem={current_selection}
141
+ item={item}
142
+ onPress={handlePress}
143
+ label={theme[item?.label] ?? item?.label}
144
+ iconBaseProps={iconBaseProps}
145
+ itemBaseProps={itemBaseProps}
146
+ labelBaseProps={labelBaseProps}
147
+ selectedItemIcon={getSelectedItemIcon(theme)}
148
+ defaultItemIcon={getDefaultItemIcon(theme)}
149
+ iconPlacement={iconPlacement}
150
+ />
151
+ ) : null
152
+ )}
130
153
  </ScrollView>
131
154
  </View>
132
155
  );
@@ -0,0 +1,74 @@
1
+ import React from "react";
2
+ import { Text } from "react-native";
3
+ import { render, screen } from "@testing-library/react-native";
4
+ import { BottomSheetDragArea } from "../BottomSheetDragArea";
5
+ import { BottomSheetContext } from "@applicaster/zapp-react-native-utils/bottomSheet";
6
+
7
+ // Reveal PanGestureHandler and capture its props.
8
+ jest.mock("react-native-gesture-handler", () => {
9
+ const { View } = require("react-native");
10
+ const actual = jest.requireActual("react-native-gesture-handler");
11
+
12
+ return {
13
+ __esModule: true,
14
+ ...actual,
15
+ PanGestureHandler: ({
16
+ children,
17
+ onGestureEvent,
18
+ onHandlerStateChange,
19
+ simultaneousHandlers,
20
+ }: any) => (
21
+ <View
22
+ testID="drag-pan"
23
+ onGestureEvent={onGestureEvent}
24
+ onHandlerStateChange={onHandlerStateChange}
25
+ simultaneousHandlers={simultaneousHandlers}
26
+ >
27
+ {children}
28
+ </View>
29
+ ),
30
+ };
31
+ });
32
+
33
+ const makeValue = () => ({
34
+ scrollRef: React.createRef<any>(),
35
+ masterDrawerRef: React.createRef<any>(),
36
+ drawerRef: React.createRef<any>(),
37
+ onScroll: jest.fn(),
38
+ setBodyDragEnabled: jest.fn(),
39
+ onDragGestureEvent: jest.fn(),
40
+ onDragStateChange: jest.fn(),
41
+ });
42
+
43
+ describe("BottomSheetDragArea", () => {
44
+ it("wraps children in a sheet-wired PanGestureHandler when inside a sheet", () => {
45
+ const value = makeValue();
46
+
47
+ render(
48
+ <BottomSheetContext.Provider value={value}>
49
+ <BottomSheetDragArea>
50
+ <Text testID="child">hi</Text>
51
+ </BottomSheetDragArea>
52
+ </BottomSheetContext.Provider>
53
+ );
54
+
55
+ const pan = screen.getByTestId("drag-pan");
56
+ expect(pan.props.onGestureEvent).toBe(value.onDragGestureEvent);
57
+ expect(pan.props.onHandlerStateChange).toBe(value.onDragStateChange);
58
+
59
+ expect(pan.props.simultaneousHandlers).toEqual([value.masterDrawerRef]);
60
+
61
+ expect(screen.getByTestId("child")).toBeTruthy();
62
+ });
63
+
64
+ it("renders children bare when used outside a sheet", () => {
65
+ render(
66
+ <BottomSheetDragArea>
67
+ <Text testID="child">hi</Text>
68
+ </BottomSheetDragArea>
69
+ );
70
+
71
+ expect(screen.queryByTestId("drag-pan")).toBeNull();
72
+ expect(screen.getByTestId("child")).toBeTruthy();
73
+ });
74
+ });
@@ -1,88 +1,7 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`OfflineHandler renders 1`] = `
4
- <View
5
- style={
6
- {
7
- "flex": 1,
8
- "position": "relative",
9
- }
10
- }
11
- >
12
- <View
13
- style={
14
- {
15
- "bottom": 0,
16
- "left": 0,
17
- "position": "absolute",
18
- "right": 0,
19
- "top": 0,
20
- }
21
- }
22
- >
23
- <View
24
- accessibilityState={
25
- {
26
- "busy": undefined,
27
- "checked": undefined,
28
- "disabled": undefined,
29
- "expanded": undefined,
30
- "selected": undefined,
31
- }
32
- }
33
- accessible={true}
34
- collapsable={false}
35
- focusable={true}
36
- onClick={[Function]}
37
- onResponderGrant={[Function]}
38
- onResponderMove={[Function]}
39
- onResponderRelease={[Function]}
40
- onResponderTerminate={[Function]}
41
- onResponderTerminationRequest={[Function]}
42
- onStartShouldSetResponder={[Function]}
43
- style={
44
- {
45
- "alignItems": "center",
46
- "backgroundColor": undefined,
47
- "flex": 1,
48
- "height": 88,
49
- "justifyContent": "center",
50
- "left": 0,
51
- "opacity": 0,
52
- "position": "absolute",
53
- "right": 0,
54
- "top": 0,
55
- "transform": [
56
- {
57
- "translateY": -88,
58
- },
59
- ],
60
- "width": "100%",
61
- }
62
- }
63
- >
64
- <View
65
- style={
66
- {
67
- "backgroundColor": undefined,
68
- "height": 44,
69
- }
70
- }
71
- />
72
- <Text
73
- style={
74
- {
75
- "color": undefined,
76
- "fontFamily": undefined,
77
- "fontSize": undefined,
78
- "letterSpacing": undefined,
79
- "lineHeight": undefined,
80
- }
81
- }
82
- >
83
- You are back online
84
- </Text>
85
- </View>
86
- </View>
87
- </View>
3
+ exports[`OfflineHandler renders its children without wrapping them in a notification view 1`] = `
4
+ <Text>
5
+ child content
6
+ </Text>
88
7
  `;
@@ -1,26 +1,26 @@
1
1
  import React from "react";
2
+ import { Text } from "react-native";
2
3
  import { render } from "@testing-library/react-native";
4
+ import { subscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
3
5
 
4
6
  jest.useFakeTimers({ legacyFakeTimers: true });
5
7
 
8
+ // `isOnline` (@applicaster/quick-brick-core/App/NetworkStatusProvider/utils)
9
+ // only treats connectionInfo as "ready" when it has NetInfoState-shaped
10
+ // isConnected/isInternetReachable/details fields, so the mock has to mirror
11
+ // that shape for online/offline to actually be distinguishable.
6
12
  let mockConnectionStatus = false;
7
- const mockPreviousValue = null;
8
13
 
9
14
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/connection", () => {
10
15
  return {
11
16
  ...jest.requireActual(
12
17
  "@applicaster/zapp-react-native-utils/reactHooks/connection"
13
18
  ),
14
- useConnectionInfo: jest.fn(() => mockConnectionStatus),
15
- };
16
- });
17
-
18
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => {
19
- return {
20
- ...jest.requireActual(
21
- "@applicaster/zapp-react-native-utils/reactHooks/utils"
22
- ),
23
- usePrevious: jest.fn(() => mockPreviousValue),
19
+ useConnectionInfo: jest.fn(() => ({
20
+ details: {},
21
+ isConnected: mockConnectionStatus,
22
+ isInternetReachable: mockConnectionStatus,
23
+ })),
24
24
  };
25
25
  });
26
26
 
@@ -54,21 +54,81 @@ jest.mock("react-native-safe-area-context", () => ({
54
54
  }));
55
55
 
56
56
  describe("OfflineHandler", () => {
57
- const { OfflineHandler, NotificationView } = require("../");
57
+ const { OfflineHandler } = require("../");
58
+
59
+ beforeEach(() => {
60
+ mockConnectionStatus = false;
61
+ });
62
+
63
+ it("renders its children without wrapping them in a notification view", () => {
64
+ const { toJSON } = render(
65
+ <OfflineHandler>
66
+ <Text>child content</Text>
67
+ </OfflineHandler>
68
+ );
58
69
 
59
- it("renders", () => {
60
- const { toJSON } = render(<OfflineHandler />);
61
70
  expect(toJSON()).toMatchSnapshot();
62
71
  });
63
72
 
64
- it("renders Notification mode if component was rendered while online", () => {
73
+ it("emits a showToast event on mount when the device starts offline", () => {
74
+ mockConnectionStatus = false;
75
+
76
+ const handleShowToast = jest.fn();
77
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
78
+
79
+ render(<OfflineHandler />);
80
+
81
+ expect(handleShowToast).toHaveBeenCalledWith(
82
+ expect.objectContaining({
83
+ id: "offline-status",
84
+ message: "No internet connection",
85
+ timeout: 5000,
86
+ }),
87
+ // the shared event bus appends a dispose fn as the last handler arg
88
+ expect.any(Function)
89
+ );
90
+
91
+ unsubscribe();
92
+ });
93
+
94
+ it("does not emit a showToast event on mount when the device starts online", () => {
65
95
  mockConnectionStatus = true;
66
96
 
67
- const { rerender, UNSAFE_getByType } = render(<OfflineHandler />);
97
+ const handleShowToast = jest.fn();
98
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
99
+
100
+ render(<OfflineHandler />);
101
+
102
+ expect(handleShowToast).not.toHaveBeenCalled();
103
+
104
+ unsubscribe();
105
+ });
106
+
107
+ it("emits a showToast event when the device comes back online", () => {
108
+ mockConnectionStatus = false;
109
+
110
+ const handleShowToast = jest.fn();
111
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
112
+
113
+ const { rerender } = render(<OfflineHandler />);
68
114
 
115
+ // ignore the offline toast emitted on mount; we only care about the
116
+ // offline -> online transition here
117
+ handleShowToast.mockClear();
118
+
119
+ mockConnectionStatus = true;
69
120
  rerender(<OfflineHandler />);
70
121
 
71
- const notificationsView = UNSAFE_getByType(NotificationView);
72
- expect(notificationsView).toBeDefined();
122
+ expect(handleShowToast).toHaveBeenCalledWith(
123
+ expect.objectContaining({
124
+ id: "offline-status",
125
+ message: "You are back online",
126
+ timeout: 2000,
127
+ }),
128
+ // the shared event bus appends a dispose fn as the last handler arg
129
+ expect.any(Function)
130
+ );
131
+
132
+ unsubscribe();
73
133
  });
74
134
  });
@@ -0,0 +1,123 @@
1
+ import React, { useMemo } from "react";
2
+
3
+ import {
4
+ isWeb,
5
+ platformSelect,
6
+ } from "@applicaster/zapp-react-native-utils/reactUtils";
7
+ import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
8
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
9
+
10
+ import { usePlugins } from "@applicaster/zapp-react-native-redux";
11
+ import { textTransform } from "@applicaster/zapp-react-native-utils/cellUtils";
12
+ import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
13
+
14
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
15
+
16
+ const OFFLINE_EXPERIENCE = "offline-experience";
17
+
18
+ const NETWORK_STATUS_LOCALIZATIONS_KEY = "network_status_localizations";
19
+
20
+ const THEME_STORAGE_NAMESPACE = "quick-brick-theme";
21
+
22
+ export const useNotificationHeight = () => {
23
+ const insets = useSafeAreaInsets();
24
+
25
+ const navBarHeight = platformSelect({ ios: 44, android: 56 });
26
+
27
+ const statusHeight = insets.top;
28
+ const notificationHeight = statusHeight + navBarHeight;
29
+
30
+ return { statusHeight, notificationHeight };
31
+ };
32
+
33
+ export function useNetworkStatusLocalizations() {
34
+ const [storedLocalizations, setStoredLocalizations] = React.useState<Record<
35
+ string,
36
+ string
37
+ > | null>(null);
38
+
39
+ React.useEffect(() => {
40
+ async function loadStoredLocalizations() {
41
+ try {
42
+ const stored = await sessionStorage.getItem(
43
+ NETWORK_STATUS_LOCALIZATIONS_KEY,
44
+ THEME_STORAGE_NAMESPACE
45
+ );
46
+
47
+ if (stored) {
48
+ setStoredLocalizations(stored);
49
+ }
50
+ } catch (error) {
51
+ console.error("Error loading network status localizations", error);
52
+ }
53
+ }
54
+
55
+ loadStoredLocalizations();
56
+ }, []);
57
+
58
+ return storedLocalizations;
59
+ }
60
+
61
+ export function useOfflineHandlerProps(online: boolean) {
62
+ const theme = useTheme<BaseThemePropertiesTV>();
63
+ const storedLocalizations = useNetworkStatusLocalizations();
64
+ const plugins = usePlugins();
65
+ const offlinePlugin = findPluginByIdentifier(OFFLINE_EXPERIENCE, plugins);
66
+ const { useOfflineExperienceConfiguration } = offlinePlugin?.module ?? {};
67
+
68
+ const {
69
+ configurationFields,
70
+ localizations,
71
+ }: {
72
+ configurationFields: OfflineExperiencePluginConfiguration;
73
+ localizations: OfflineExperienceLocalizations;
74
+ } = useOfflineExperienceConfiguration?.() ?? {};
75
+
76
+ let title, subtitle, style;
77
+
78
+ if (isWeb()) {
79
+ title = online
80
+ ? (theme?.online_notification_title ??
81
+ storedLocalizations?.online_notification_title)
82
+ : (theme?.offline_notification_title ??
83
+ storedLocalizations?.offline_notification_title);
84
+
85
+ subtitle = online
86
+ ? (theme?.online_notification_subtitle ??
87
+ storedLocalizations?.online_notification_subtitle)
88
+ : (theme?.offline_notification_subtitle ??
89
+ storedLocalizations?.offline_notification_subtitle);
90
+ } else {
91
+ title = textTransform(
92
+ configurationFields.offline_toast_message_text_transform,
93
+ online
94
+ ? localizations.online_toast_message
95
+ : localizations.offline_toast_message
96
+ );
97
+
98
+ style = {
99
+ color: online
100
+ ? configurationFields.offline_toast_message_online_font_color
101
+ : configurationFields.offline_toast_message_font_color,
102
+ backgroundColor: online
103
+ ? configurationFields.offline_toast_online_background_color
104
+ : configurationFields.offline_toast_offline_background_color,
105
+ fontFamily: platformSelect({
106
+ ios: configurationFields.offline_toast_message_ios_font_family,
107
+ android: configurationFields.offline_toast_message_android_font_family,
108
+ }),
109
+ fontSize: configurationFields.offline_toast_message_font_size,
110
+ lineHeight: configurationFields.offline_toast_message_line_height,
111
+ letterSpacing: platformSelect({
112
+ ios: configurationFields.offline_toast_message_letter_spacing_ios,
113
+ android:
114
+ configurationFields.offline_toast_message_letter_spacing_android,
115
+ }),
116
+ };
117
+ }
118
+
119
+ return useMemo(
120
+ () => ({ message: title, extraMessage: subtitle, style }),
121
+ [title, subtitle, style]
122
+ );
123
+ }