@applicaster/zapp-react-native-ui-components 15.1.0-rc.8 → 16.0.0-alpha.1011304583

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 (178) 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/BaseFocusable/index.ios.ts +12 -2
  6. package/Components/Cell/FocusableWrapper.tsx +3 -0
  7. package/Components/Cell/TvOSCellComponent.tsx +6 -3
  8. package/Components/CellRendererResolver/index.ts +2 -2
  9. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  10. package/Components/Focusable/Focusable.tsx +4 -2
  11. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  12. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  13. package/Components/FocusableGroup/FocusableTvOS.tsx +41 -8
  14. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
  15. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  16. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  17. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  18. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  19. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +124 -0
  20. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  21. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  22. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  23. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +72 -0
  24. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  25. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  26. package/Components/GeneralContentScreen/index.ts +2 -0
  27. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  28. package/Components/HandlePlayable/HandlePlayable.tsx +13 -8
  29. package/Components/Layout/FullWidthRow.tsx +38 -0
  30. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  31. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  32. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  33. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  34. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +0 -1
  35. package/Components/Layout/TV/index.tsx +5 -7
  36. package/Components/Layout/TV/index.web.tsx +5 -7
  37. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  38. package/Components/MasterCell/CONFIG_BUILDER_TO_REACT_COMPONENT.md +144 -0
  39. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +16 -5
  40. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  41. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  42. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  43. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  44. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +165 -0
  45. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +405 -0
  46. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/index.ts +1 -0
  47. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  48. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  49. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  50. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  51. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  52. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +4 -10
  53. package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
  54. package/Components/MasterCell/DefaultComponents/ButtonContainerView/components/HorizontalSeparator.tsx +8 -0
  55. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.android.tv.tsx +58 -0
  56. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +15 -0
  57. package/Components/MasterCell/DefaultComponents/{tv/ButtonContainerView/index.tsx → ButtonContainerView/index.tv.tsx} +3 -11
  58. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.web.ts +1 -0
  59. package/Components/MasterCell/DefaultComponents/ButtonContainerView/types.ts +40 -0
  60. package/Components/MasterCell/DefaultComponents/DataProvider/index.tsx +163 -0
  61. package/Components/MasterCell/DefaultComponents/FocusableView/index.android.tsx +2 -23
  62. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -22
  63. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +8 -2
  64. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  65. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  66. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  67. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +1 -2
  68. package/Components/MasterCell/DefaultComponents/PressableView.tsx +34 -0
  69. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  70. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +11 -0
  71. package/Components/MasterCell/DefaultComponents/__tests__/DataProvider.test.tsx +141 -0
  72. package/Components/MasterCell/DefaultComponents/index.ts +9 -3
  73. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +135 -0
  74. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +33 -0
  75. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +22 -0
  76. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +125 -0
  77. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  78. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  79. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +37 -0
  80. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +393 -0
  81. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +141 -0
  82. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +343 -0
  83. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  84. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +122 -0
  85. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  86. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +238 -0
  87. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Asset.ts +4 -18
  88. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Button.ts +24 -73
  89. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TextLabelsContainer.ts +37 -18
  90. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TvActionButton.tsx +27 -0
  91. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +89 -0
  92. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/renderedTree.test.tsx +231 -0
  93. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +47 -48
  94. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +115 -29
  95. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +101 -144
  96. package/Components/MasterCell/MappingFunctions/index.js +3 -2
  97. package/Components/MasterCell/README.md +4 -0
  98. package/Components/MasterCell/__tests__/__snapshots__/dataAdapter.test.js.snap +24 -0
  99. package/Components/MasterCell/__tests__/configInflater.test.js +1 -0
  100. package/Components/MasterCell/__tests__/elementMapper.test.js +46 -0
  101. package/Components/MasterCell/dataAdapter.ts +19 -6
  102. package/Components/MasterCell/elementMapper.tsx +52 -7
  103. package/Components/MasterCell/index.tsx +30 -5
  104. package/Components/MasterCell/utils/__tests__/cloneChildrenWithIds.test.tsx +43 -0
  105. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  106. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  107. package/Components/MasterCell/utils/__tests__/useFilterChildren.test.tsx +80 -0
  108. package/Components/MasterCell/utils/index.ts +170 -61
  109. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  110. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  111. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  112. package/Components/PlayerContainer/PlayerContainer.tsx +22 -17
  113. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +284 -0
  114. package/Components/River/ComponentsMap/ComponentsMap.tsx +8 -21
  115. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  116. package/Components/River/RefreshControl.tsx +19 -88
  117. package/Components/River/River.tsx +9 -82
  118. package/Components/River/TV/River.tsx +31 -14
  119. package/Components/River/TV/index.tsx +8 -4
  120. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  121. package/Components/River/TV/utils/index.ts +4 -0
  122. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  123. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -15
  124. package/Components/River/__tests__/componentsMap.test.js +38 -0
  125. package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
  126. package/Components/River/hooks/index.ts +1 -0
  127. package/Components/River/hooks/usePullToRefresh.ts +51 -0
  128. package/Components/Screen/TV/index.web.tsx +4 -2
  129. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  130. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  131. package/Components/Screen/hooks.ts +2 -3
  132. package/Components/Screen/index.tsx +10 -7
  133. package/Components/Screen/orientationHandler.ts +3 -3
  134. package/Components/ScreenResolver/index.tsx +9 -5
  135. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  136. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  137. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  138. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  139. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +9 -20
  140. package/Components/Tabs/TabContent.tsx +7 -4
  141. package/Components/TopCutoffOverlay/__tests__/TopCutoffOverlay.test.tsx +201 -0
  142. package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
  143. package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
  144. package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
  145. package/Components/TopCutoffOverlay/index.tsx +55 -0
  146. package/Components/Transitioner/index.js +3 -3
  147. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +5 -5
  148. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  149. package/Components/VideoModal/utils.ts +18 -10
  150. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
  151. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  152. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  153. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +9 -4
  154. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  155. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  156. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  157. package/Contexts/ScreenContext/index.tsx +25 -18
  158. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  159. package/Decorators/Analytics/index.tsx +6 -5
  160. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  161. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  162. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
  163. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  164. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  165. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  166. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
  167. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  168. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  169. package/Helpers/DataSourceHelper/index.ts +19 -0
  170. package/Helpers/index.js +7 -40
  171. package/package.json +5 -5
  172. package/Components/Layout/TV/LayoutBackground.tsx +0 -28
  173. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +0 -135
  174. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/types.ts +0 -25
  175. package/Components/River/TV/withTVEventHandler.tsx +0 -36
  176. package/Components/Screen/utils.ts +0 -16
  177. package/Helpers/Analytics/index.js +0 -95
  178. package/Helpers/DataSourceHelper/index.js +0 -19
@@ -0,0 +1,80 @@
1
+ import React from "react";
2
+ import { renderHook } from "@testing-library/react-native";
3
+ import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
4
+
5
+ import { DataProvider } from "../../DefaultComponents/DataProvider";
6
+ import { useFilterChildren } from "..";
7
+
8
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
9
+ useActions: jest.fn(),
10
+ }));
11
+
12
+ const mockUseActions = useActions as jest.Mock;
13
+ const Probe = () => null;
14
+
15
+ describe("useFilterChildren", () => {
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ });
19
+
20
+ it("filters DataProvider-wrapped button children using the wrapped button props", () => {
21
+ const available = jest.fn(() => true);
22
+ const unavailable = jest.fn(() => false);
23
+
24
+ mockUseActions.mockReturnValue({
25
+ actions: {
26
+ available_action: {
27
+ module: {
28
+ context: {
29
+ _currentValue: {
30
+ isActionAvailable: available,
31
+ },
32
+ },
33
+ },
34
+ },
35
+ unavailable_action: {
36
+ module: {
37
+ context: {
38
+ _currentValue: {
39
+ isActionAvailable: unavailable,
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ });
46
+
47
+ const availableItem = { id: "entry-1" };
48
+ const unavailableItem = { id: "entry-2" };
49
+
50
+ const children = [
51
+ <DataProvider key="available" entry={availableItem}>
52
+ <Probe
53
+ entry={availableItem}
54
+ pluginIdentifier="available_action"
55
+ suffixId="button_1"
56
+ cellUUID="cell-1"
57
+ />
58
+ </DataProvider>,
59
+ <DataProvider key="unavailable" entry={unavailableItem}>
60
+ <Probe
61
+ entry={unavailableItem}
62
+ pluginIdentifier="unavailable_action"
63
+ suffixId="button_2"
64
+ cellUUID="cell-1"
65
+ />
66
+ </DataProvider>,
67
+ ];
68
+
69
+ const { result } = renderHook(() => useFilterChildren(children));
70
+
71
+ expect(result.current).toHaveLength(1);
72
+
73
+ expect(result.current[0].props.children.props.pluginIdentifier).toBe(
74
+ "available_action"
75
+ );
76
+
77
+ expect(available).toHaveBeenCalledWith(availableItem);
78
+ expect(unavailable).toHaveBeenCalledWith(unavailableItem);
79
+ });
80
+ });
@@ -1,21 +1,23 @@
1
1
  import React, { useMemo } from "react";
2
2
  import * as R from "ramda";
3
- import validateColor from "validate-color";
3
+ import memoizee from "memoizee";
4
+
4
5
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
5
6
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
6
-
7
- import { masterCellLogger } from "../logger";
8
- import { getCellState } from "../../Cell/utils";
7
+ import { isValidColor } from "@applicaster/zapp-react-native-utils/colorUtils";
9
8
  import { getColorFromData } from "@applicaster/zapp-react-native-utils/cellUtils";
10
- import { get } from "@applicaster/zapp-react-native-utils/utils";
11
- import { isCellSelected, useBehaviorUpdate } from "./behaviorProvider";
9
+ import { pathOr, get, isNil } from "@applicaster/zapp-react-native-utils/utils";
10
+ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
12
11
  import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks";
13
12
  import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
14
- import memoizee from "memoizee";
15
- import stringify from "fast-json-stable-stringify";
13
+ import { isNotEmptyString } from "@applicaster/zapp-react-native-utils/stringUtils";
14
+ import { masterCellLogger } from "../logger";
15
+ import { getCellState } from "../../Cell/utils";
16
+ import { isCellSelected, useBehaviorUpdate } from "./behaviorProvider";
17
+ import { DataProvider } from "../DefaultComponents/DataProvider";
16
18
 
17
19
  const hasElementSpecificViewType = (viewType) => (element) => {
18
- if (R.isNil(element)) {
20
+ if (isNil(element)) {
19
21
  return false;
20
22
  }
21
23
 
@@ -27,12 +29,20 @@ const hasElementSpecificViewType = (viewType) => (element) => {
27
29
  return hasElementsSpecificViewType(viewType)(element.elements);
28
30
  };
29
31
 
32
+ export const hasElementsSpecificViewType = (viewType) => (elements) => {
33
+ if (isNilOrEmpty(elements)) {
34
+ return false;
35
+ }
36
+
37
+ return R.any(hasElementSpecificViewType(viewType))(elements);
38
+ };
39
+
30
40
  const logWarning = (
31
41
  colorValueFromCellStyle,
32
42
  colorFromProp,
33
43
  colorValueFromEntry
34
44
  ) => {
35
- if (R.isNil(colorValueFromEntry)) {
45
+ if (isNil(colorValueFromEntry)) {
36
46
  masterCellLogger.warn({
37
47
  message: `Cannot resolve property ${colorValueFromCellStyle} from the entry.`,
38
48
  data: {
@@ -43,15 +53,10 @@ const logWarning = (
43
53
  }
44
54
  };
45
55
 
46
- export const hasElementsSpecificViewType = (viewType) => (elements) => {
47
- if (R.isNil(elements) || R.isEmpty(elements)) {
48
- return false;
49
- }
50
-
51
- return R.any(hasElementSpecificViewType(viewType))(elements);
52
- };
53
-
54
- function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
56
+ export function resolveColorForProp(
57
+ entry: any,
58
+ colorFromProp: string | undefined
59
+ ) {
55
60
  if (!colorFromProp) {
56
61
  return undefined;
57
62
  }
@@ -61,9 +66,7 @@ function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
61
66
  colorFromProp.split(".")
62
67
  );
63
68
 
64
- const color = colorFromProp.replace(".00", "").replace(".0", ""); // https://github.com/dreamyguy/validate-color/issues/44
65
-
66
- if (nestedEntryValue === undefined && !validateColor(color)) {
69
+ if (nestedEntryValue === undefined && !isValidColor(colorFromProp)) {
67
70
  logWarning(colorFromProp, colorFromProp, nestedEntryValue);
68
71
 
69
72
  return undefined;
@@ -85,36 +88,73 @@ function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
85
88
 
86
89
  const getColorKeys = memoizee((style) => {
87
90
  const styleKeys = Object.keys(style);
88
- const colorKeys = styleKeys.filter((key) => /color/i.test(key));
89
91
 
90
- return colorKeys;
92
+ return styleKeys.filter((key) => /color/i.test(key));
91
93
  });
92
94
 
93
- export const resolveColor = memoizee(
94
- (entry, style) => {
95
- if (style === null || style === undefined) {
96
- return style;
97
- }
95
+ /**
96
+ * A color style value is either a literal color (e.g. "#FFFFFF") or a
97
+ * data-mapping path into the entry. Data mappings always point at the entry
98
+ * `extensions` object (e.g. "extensions.brandColor"), so we only need to do a
99
+ * path lookup when the value starts with "extensions" — everything else is a
100
+ * literal color and can be returned as-is.
101
+ *
102
+ * This avoids the previous, very expensive approach that ran `validateColor`
103
+ * on every value and memoized on a full `fast-json-stable-stringify` of the
104
+ * entire entry.
105
+ */
106
+ const EXTENSIONS_PREFIX = "extensions";
107
+
108
+ const isDataMappingPath = (value: string): boolean =>
109
+ typeof value === "string" && value.startsWith(EXTENSIONS_PREFIX);
110
+
111
+ export const resolveColor = (
112
+ entry,
113
+ style,
114
+ allowDynamicColorsOutsideExtensions
115
+ ) => {
116
+ if (style === null || style === undefined) {
117
+ return style;
118
+ }
119
+
120
+ const colorKeys = getColorKeys(style);
121
+
122
+ if (colorKeys.length === 0) {
123
+ return style;
124
+ }
125
+
126
+ return colorKeys.reduce(
127
+ (acc, key) => {
128
+ const value = acc[key];
98
129
 
99
- return getColorKeys(style).reduce(
100
- (acc, value) => {
101
- if (acc[value] && typeof acc[value] === "string") {
102
- const colorStyle = resolveColorForProp(entry, acc[value]);
130
+ // 1. The Expensive Edge-Case (Only for the app with root mappings)
131
+ if (allowDynamicColorsOutsideExtensions && isNotEmptyString(value)) {
132
+ const possibleColor = resolveColorForProp(entry, value);
103
133
 
104
- acc[value] = colorStyle;
105
- }
134
+ acc[key] = isValidColor(possibleColor) ? possibleColor : null;
106
135
 
107
136
  return acc;
108
- },
109
- { ...style }
110
- );
111
- },
112
- {
113
- normalizer: (args) => {
114
- return [stringify(args[0]), stringify(args[1])].join("|");
137
+ }
138
+
139
+ // 2. The Fast Path (For 99% of apps)
140
+ if (isDataMappingPath(value)) {
141
+ // resolve the mapped color from the entry; fall back to null
142
+ // (RN ignores null style values) when the path is missing
143
+ const possibleColor = pathOr(null, value.split("."), entry);
144
+
145
+ acc[key] = possibleColor;
146
+
147
+ return acc;
148
+ }
149
+
150
+ // 3. Default Case: Treat as a raw color string
151
+ acc[key] = value;
152
+
153
+ return acc;
115
154
  },
116
- }
117
- );
155
+ { ...style }
156
+ );
157
+ };
118
158
 
119
159
  export function isVideoPreviewEnabled({
120
160
  enable_video_preview = false,
@@ -126,10 +166,50 @@ export function isVideoPreviewEnabled({
126
166
  return enable_video_preview && !R.isEmpty(player_screen_id);
127
167
  }
128
168
 
169
+ export const unwrapDataProviderChild = (
170
+ child: React.ReactElement
171
+ ): React.ReactElement => {
172
+ if ((child as any)?.type !== DataProvider) {
173
+ return child;
174
+ }
175
+
176
+ const [wrappedChild] = React.Children.toArray((child as any).props.children);
177
+
178
+ return (
179
+ React.isValidElement(wrappedChild) ? wrappedChild : child
180
+ ) as React.ReactElement;
181
+ };
182
+
183
+ /**
184
+ * Resolves the entry value passed by DataProvider.
185
+ * Prefer the direct `entry` prop and fall back to `dataProviderProps[_dataKey]`
186
+ * for older wrappers that still read from namespaced provider props.
187
+ */
188
+ const getEntryFromProps = (props: {
189
+ dataProviderProps?: {
190
+ _dataKey: string;
191
+ [key: string]: unknown;
192
+ };
193
+ entry?: any;
194
+ }) => {
195
+ const dataProviderProps = props.dataProviderProps;
196
+
197
+ return (
198
+ props.entry ??
199
+ (dataProviderProps?._dataKey
200
+ ? dataProviderProps[dataProviderProps._dataKey]
201
+ : undefined)
202
+ );
203
+ };
204
+
129
205
  export const useFilterChildren = <
130
206
  T extends {
131
207
  props: {
132
- item: any;
208
+ dataProviderProps?: {
209
+ _dataKey: string;
210
+ [key: string]: unknown;
211
+ };
212
+ entry?: any;
133
213
  pluginIdentifier: string;
134
214
  };
135
215
  },
@@ -139,8 +219,13 @@ export const useFilterChildren = <
139
219
  const actions = useActions("");
140
220
 
141
221
  const filteredChildren = children.filter((child) => {
142
- const item = child.props.item;
143
- const actionIdentifier = child.props.pluginIdentifier;
222
+ const wrappedChild = unwrapDataProviderChild(
223
+ child as unknown as React.ReactElement
224
+ ) as unknown as T;
225
+
226
+ const item = getEntryFromProps(wrappedChild.props);
227
+
228
+ const actionIdentifier = wrappedChild.props.pluginIdentifier;
144
229
  const action = actions.actions[actionIdentifier];
145
230
 
146
231
  // context value of specific plugin
@@ -154,7 +239,7 @@ export const useFilterChildren = <
154
239
 
155
240
  masterCellLogger.error({
156
241
  message: `Action plugin for ${actionIdentifier} could not be found, check the configuration of your action button`,
157
- data: { item, action: child.props.pluginIdentifier },
242
+ data: { item, action: wrappedChild.props.pluginIdentifier },
158
243
  });
159
244
 
160
245
  return false;
@@ -192,7 +277,7 @@ const recursiveCloneElement = (focused: boolean) => (element) => {
192
277
  };
193
278
 
194
279
  export const recursiveCloneElementsWithState = (focused: boolean, children) => {
195
- if (R.isNil(children) || R.isEmpty(children)) {
280
+ if (isNilOrEmpty(children)) {
196
281
  return undefined;
197
282
  }
198
283
 
@@ -202,17 +287,42 @@ export const recursiveCloneElementsWithState = (focused: boolean, children) => {
202
287
  const next = (currentIndex, items) => items[currentIndex + 1];
203
288
  const previous = (currentIndex, items) => items[currentIndex - 1];
204
289
 
205
- export const cloneElementsWithIds = (ids, children) => {
206
- if (R.isNil(children) || R.isEmpty(children)) {
290
+ export const cloneChildrenWithIds = (
291
+ ids: string[],
292
+ children: React.ReactElement[]
293
+ ) => {
294
+ if (isNilOrEmpty(children)) {
207
295
  return undefined;
208
296
  }
209
297
 
210
- return React.Children.map(children, (element, index) =>
211
- React.cloneElement(element, {
212
- nextFocusLeft: previous(index, ids),
213
- nextFocusRight: next(index, ids),
214
- })
215
- );
298
+ return React.Children.map(children, (element, index) => {
299
+ const nextFocusLeft = previous(index, ids);
300
+ const nextFocusRight = next(index, ids);
301
+
302
+ if (element?.type !== DataProvider) {
303
+ return React.cloneElement(element, {
304
+ nextFocusLeft,
305
+ nextFocusRight,
306
+ });
307
+ }
308
+
309
+ const wrappedChild = unwrapDataProviderChild(element) as React.ReactElement<
310
+ Record<string, unknown>
311
+ >;
312
+
313
+ if (!React.isValidElement(wrappedChild)) {
314
+ return element;
315
+ }
316
+
317
+ const injectedWrappedChild = React.cloneElement(wrappedChild, {
318
+ nextFocusLeft,
319
+ nextFocusRight,
320
+ });
321
+
322
+ return React.cloneElement(element, {
323
+ children: injectedWrappedChild,
324
+ });
325
+ });
216
326
  };
217
327
 
218
328
  export const getFocusedButtonId = (focusable) => {
@@ -252,10 +362,9 @@ export const useCellState = ({
252
362
  export const hasFocusableInsideBuilder = (elementsBuilder) => (item) => {
253
363
  const elements = elementsBuilder({ entry: item });
254
364
 
255
- return R.anyPass([
256
- hasElementsSpecificViewType("ButtonContainerView"),
257
- hasElementsSpecificViewType("FocusableView"),
258
- ])(elements);
365
+ return R.anyPass([hasElementsSpecificViewType("ButtonContainerView")])(
366
+ elements
367
+ );
259
368
  };
260
369
 
261
370
  export function getEntryState(state, selected) {
@@ -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", () => ({
@@ -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,
@@ -59,6 +59,7 @@ import {
59
59
  PlayerNativeCommandTypes,
60
60
  PlayerNativeSendCommand,
61
61
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
62
+ import { useAppData } from "@applicaster/zapp-react-native-redux";
62
63
 
63
64
  type Props = {
64
65
  Player: React.ComponentType<any>;
@@ -237,7 +238,7 @@ const PlayerContainerComponent = (props: Props) => {
237
238
  const [isLoadingNextVideo, setIsLoadingNextVideo] = React.useState(false);
238
239
 
239
240
  const navigator = useNavigation();
240
- const { appData } = usePickFromState(["appData"]);
241
+ const { isTabletPortrait } = useAppData();
241
242
  const prevItemId = usePrevious(item?.id);
242
243
  const screenData = useTargetScreenData(item);
243
244
  const { setVisible: showNavBar } = useSetNavbarState();
@@ -262,9 +263,14 @@ const PlayerContainerComponent = (props: Props) => {
262
263
 
263
264
  showNavBar(true);
264
265
  navigator.goBack();
265
- }, [isModal, navigator.goBack, state.playerId, showNavBar]);
266
+ }, [isModal, state.playerId, showNavBar, navigator]);
267
+
268
+ const pluginConfiguration = React.useMemo(
269
+ () => player?.getPluginConfiguration(),
270
+ [player]
271
+ );
266
272
 
267
- const playEntry = (entry) => navigator.replaceTop(entry, { mode });
273
+ const playEntry = (entry: ZappEntry) => navigator.replaceTop(entry, { mode });
268
274
 
269
275
  const onPlayNextPerformNextVideoPlay = React.useCallback(() => {
270
276
  if (!playNextOverlayState.entry) {
@@ -338,11 +344,15 @@ const PlayerContainerComponent = (props: Props) => {
338
344
  setIsLoadingNextVideo(false);
339
345
 
340
346
  const resumeTime = Number(item?.extensions?.resumeTime);
347
+ const activePlayer = playerManager.getActivePlayer();
341
348
 
342
- // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast
349
+ // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast.
350
+ // Live streams and a missing/zero resume position are skipped: seeking to 0 on
351
+ // a live stream pulls playback off the live edge into a buffer-starvation seek loop.
343
352
  if (
344
- !playerManager.getActivePlayer()?.hasResumePosition() &&
345
- !isNaN(resumeTime)
353
+ !activePlayer?.hasResumePosition() &&
354
+ resumeTime > 0 &&
355
+ !activePlayer?.isLive()
346
356
  ) {
347
357
  player?.seekTo(resumeTime);
348
358
  }
@@ -448,13 +458,6 @@ const PlayerContainerComponent = (props: Props) => {
448
458
  }
449
459
  }, []);
450
460
 
451
- const pluginConfiguration = React.useMemo(() => {
452
- return (
453
- playerManager.getPluginConfiguration() ||
454
- R.prop("__plugin_configuration", Player)
455
- );
456
- }, [playerManager.isRegistered()]);
457
-
458
461
  const disableMiniPlayer = React.useMemo(() => {
459
462
  return pluginConfiguration?.disable_mini_player_when_inline;
460
463
  }, [pluginConfiguration]);
@@ -467,6 +470,8 @@ const PlayerContainerComponent = (props: Props) => {
467
470
  if (isModal && mode === VideoModalMode.MAXIMIZED) {
468
471
  if (disableMiniPlayer) {
469
472
  navigator.closeVideoModal();
473
+ } else {
474
+ navigator.minimiseVideoModal();
470
475
  }
471
476
  }
472
477
 
@@ -654,7 +659,7 @@ const PlayerContainerComponent = (props: Props) => {
654
659
  <PlayerFocusableWrapperView
655
660
  nextFocusDown={context.bottomFocusableId}
656
661
  >
657
- {Player ? (
662
+ {!Player ? null : (
658
663
  <Player
659
664
  source={{
660
665
  uri,
@@ -669,7 +674,7 @@ const PlayerContainerComponent = (props: Props) => {
669
674
  fullscreen={mode === VideoModalMode.FULLSCREEN}
670
675
  inline={inline}
671
676
  isModal={isModal}
672
- isTabletPortrait={appData.isTabletPortrait}
677
+ isTabletPortrait={isTabletPortrait}
673
678
  muted={false}
674
679
  playableItem={item}
675
680
  playerEvent={playerEvent}
@@ -685,7 +690,7 @@ const PlayerContainerComponent = (props: Props) => {
685
690
  >
686
691
  {renderApplePlayer(applePlayerProps)}
687
692
  </Player>
688
- ) : null}
693
+ )}
689
694
  </PlayerFocusableWrapperView>
690
695
  </View>
691
696
  {/* Components container */}