@applicaster/zapp-react-native-ui-components 15.1.4 → 16.0.0-alpha.1042942411

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 (202) hide show
  1. package/Components/BackgroundImage/index.ts +1 -8
  2. package/Components/BaseFocusable/index.ios.ts +12 -2
  3. package/Components/Cell/FocusableWrapper.tsx +3 -0
  4. package/Components/Cell/TvOSCellComponent.tsx +6 -3
  5. package/Components/CellRendererResolver/index.ts +1 -1
  6. package/Components/Focusable/Focusable.tsx +4 -2
  7. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  8. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  9. package/Components/FocusableGroup/FocusableTvOS.tsx +30 -1
  10. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
  11. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  12. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +124 -0
  16. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  17. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  18. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  19. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +72 -0
  20. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  22. package/Components/GeneralContentScreen/index.ts +2 -0
  23. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  24. package/Components/HandlePlayable/HandlePlayable.tsx +13 -8
  25. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  26. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  27. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  28. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  29. package/Components/Layout/TV/index.tsx +3 -4
  30. package/Components/Layout/TV/index.web.tsx +3 -4
  31. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  32. package/Components/MasterCell/CONFIG_BUILDER_TO_REACT_COMPONENT.md +144 -0
  33. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +27 -48
  34. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  35. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +213 -0
  36. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  37. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  38. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +165 -0
  39. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +405 -0
  40. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/index.ts +1 -0
  41. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  42. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +198 -0
  43. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  44. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  45. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  46. package/Components/MasterCell/DefaultComponents/BorderContainerView/__tests__/index.test.tsx +217 -46
  47. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +50 -119
  48. package/Components/MasterCell/DefaultComponents/Button.tsx +50 -58
  49. package/Components/MasterCell/DefaultComponents/ButtonContainerView/components/HorizontalSeparator.tsx +8 -0
  50. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.android.tv.tsx +58 -0
  51. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +15 -0
  52. package/Components/MasterCell/DefaultComponents/{tv/ButtonContainerView/index.tsx → ButtonContainerView/index.tv.tsx} +3 -11
  53. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.web.ts +1 -0
  54. package/Components/MasterCell/DefaultComponents/ButtonContainerView/types.ts +40 -0
  55. package/Components/MasterCell/DefaultComponents/CellBadge/__tests__/__snapshots__/index.test.tsx.snap +18 -0
  56. package/Components/MasterCell/DefaultComponents/CellBadge/__tests__/index.test.tsx +90 -0
  57. package/Components/MasterCell/DefaultComponents/CellBadge/index.tsx +13 -14
  58. package/Components/MasterCell/DefaultComponents/DataProvider/index.tsx +163 -0
  59. package/Components/MasterCell/DefaultComponents/FocusableView/index.android.tsx +2 -23
  60. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -22
  61. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +8 -2
  62. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  63. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  64. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  65. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  66. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +1 -2
  67. package/Components/MasterCell/DefaultComponents/PressableView.tsx +54 -0
  68. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +46 -50
  69. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +47 -39
  70. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  71. package/Components/MasterCell/DefaultComponents/SecondaryImage/utils.ts +19 -6
  72. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +15 -0
  73. package/Components/MasterCell/DefaultComponents/Text/index.tsx +45 -15
  74. package/Components/MasterCell/DefaultComponents/UIKitAdapters/Badge.tsx +49 -0
  75. package/Components/MasterCell/DefaultComponents/__tests__/ActionButton.test.tsx +122 -0
  76. package/Components/MasterCell/DefaultComponents/__tests__/Button.test.tsx +186 -0
  77. package/Components/MasterCell/DefaultComponents/__tests__/DataProvider.test.tsx +141 -0
  78. package/Components/MasterCell/DefaultComponents/__tests__/DynamicBadge.test.tsx +156 -0
  79. package/Components/MasterCell/DefaultComponents/__tests__/__snapshots__/ActionButton.test.tsx.snap +60 -0
  80. package/Components/MasterCell/DefaultComponents/__tests__/text.test.tsx +196 -67
  81. package/Components/MasterCell/DefaultComponents/index.ts +13 -3
  82. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +189 -0
  83. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +33 -0
  84. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +33 -0
  85. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +159 -0
  86. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +18 -0
  87. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  88. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +39 -0
  89. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +402 -0
  90. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +141 -0
  91. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +354 -0
  92. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  93. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +129 -0
  94. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +123 -0
  95. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +238 -0
  96. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Asset.ts +4 -18
  97. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Button.ts +24 -73
  98. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TextLabelsContainer.ts +37 -18
  99. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TvActionButton.tsx +27 -0
  100. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +89 -0
  101. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/renderedTree.test.tsx +231 -0
  102. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +47 -48
  103. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +115 -29
  104. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  105. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +101 -144
  106. package/Components/MasterCell/MappingFunctions/index.js +3 -2
  107. package/Components/MasterCell/README.md +4 -0
  108. package/Components/MasterCell/__tests__/__snapshots__/dataAdapter.test.js.snap +24 -0
  109. package/Components/MasterCell/__tests__/configInflater.test.js +1 -0
  110. package/Components/MasterCell/__tests__/elementMapper.test.js +46 -0
  111. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  112. package/Components/MasterCell/dataAdapter.ts +4 -1
  113. package/Components/MasterCell/elementMapper.tsx +54 -7
  114. package/Components/MasterCell/hooks/index.ts +2 -0
  115. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  116. package/Components/MasterCell/index.tsx +20 -4
  117. package/Components/MasterCell/utils/__tests__/cloneChildrenWithIds.test.tsx +43 -0
  118. package/Components/MasterCell/utils/__tests__/useFilterChildren.test.tsx +80 -0
  119. package/Components/MasterCell/utils/index.ts +82 -16
  120. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +33 -69
  121. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +25 -47
  122. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +122 -39
  123. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +671 -0
  124. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +257 -88
  125. package/Components/ModalComponent/AudioPlayer/Components/index.ts +6 -4
  126. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  127. package/Components/ModalComponent/BottomSheetModalContent.tsx +17 -10
  128. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  129. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  130. package/Components/OfflineHandler/__tests__/index.test.tsx +101 -32
  131. package/Components/OfflineHandler/hooks.ts +123 -0
  132. package/Components/OfflineHandler/index.tsx +51 -73
  133. package/Components/OfflineHandler/utils/index.ts +1 -13
  134. package/Components/PlayerContainer/PlayerContainer.tsx +6 -5
  135. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +2 -4
  136. package/Components/PreloaderWrapper/index.tsx +1 -1
  137. package/Components/River/ComponentsMap/ComponentsMap.tsx +8 -21
  138. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  139. package/Components/River/RefreshControl.tsx +19 -105
  140. package/Components/River/River.tsx +9 -82
  141. package/Components/River/TV/River.tsx +31 -14
  142. package/Components/River/TV/index.tsx +8 -4
  143. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  144. package/Components/River/TV/utils/index.ts +4 -0
  145. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  146. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +4 -0
  147. package/Components/River/__tests__/componentsMap.test.js +38 -0
  148. package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
  149. package/Components/River/hooks/index.ts +1 -0
  150. package/Components/River/hooks/usePullToRefresh.ts +51 -0
  151. package/Components/Screen/TV/index.web.tsx +4 -2
  152. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  153. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  154. package/Components/Screen/hooks.ts +2 -3
  155. package/Components/Screen/index.tsx +2 -3
  156. package/Components/Screen/orientationHandler.ts +3 -3
  157. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  158. package/Components/ScreenResolver/index.tsx +9 -5
  159. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  160. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  161. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  162. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +1 -1
  163. package/Components/Tabs/TabContent.tsx +7 -4
  164. package/Components/TopCutoffOverlay/__tests__/TopCutoffOverlay.test.tsx +201 -0
  165. package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
  166. package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
  167. package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
  168. package/Components/TopCutoffOverlay/index.tsx +55 -0
  169. package/Components/Transitioner/index.js +3 -3
  170. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +5 -5
  171. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  172. package/Components/VideoModal/utils.ts +12 -10
  173. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
  174. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  175. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  176. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  177. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  178. package/Contexts/ScreenContext/index.tsx +25 -18
  179. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  180. package/Decorators/Analytics/index.tsx +6 -5
  181. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  182. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  183. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
  184. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  185. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  186. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  187. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
  188. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  189. package/Helpers/DataSourceHelper/index.ts +19 -0
  190. package/package.json +5 -5
  191. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +0 -135
  192. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/types.ts +0 -25
  193. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  194. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  195. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  196. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -67
  197. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  198. package/Components/River/TV/withTVEventHandler.tsx +0 -36
  199. package/Helpers/DataSourceHelper/index.js +0 -19
  200. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  201. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  202. /package/Components/BackgroundImage/{BackgroundImage.tv.web.tsx → BackgroundImage.tsx} +0 -0
@@ -0,0 +1,165 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
9
+
10
+ type ActionDefinition = {
11
+ identifier?: string;
12
+ };
13
+
14
+ type ControllerRenderProps = {
15
+ actionContext: any;
16
+ actionState: any;
17
+ entry: any;
18
+ isActive: boolean;
19
+ onPress: () => Promise<unknown> | unknown;
20
+ };
21
+
22
+ type Props = {
23
+ action?: ActionDefinition;
24
+ pluginIdentifier?: string;
25
+ entry?: any;
26
+ onMissingActionContext?: (identifier?: string) => void;
27
+ children: (props: ControllerRenderProps) => React.ReactNode;
28
+ };
29
+
30
+ const resolveIsActive = (actionState: any, fallbackSelected = false) => {
31
+ if (actionState == null) {
32
+ return fallbackSelected;
33
+ }
34
+
35
+ return Boolean(
36
+ actionState?.active ??
37
+ actionState?.isActive ??
38
+ actionState?.selected ??
39
+ actionState?.isSelected ??
40
+ fallbackSelected
41
+ );
42
+ };
43
+
44
+ const buildLegacySelection = (entry: any, actionContext: any) => {
45
+ const defaultIsSelected = Array.isArray(actionContext?.state)
46
+ ? (actionContext?.state || []).includes(entry)
47
+ : false;
48
+
49
+ return actionContext?.masterCell?.isSelected
50
+ ? actionContext.masterCell.isSelected(entry)
51
+ : defaultIsSelected;
52
+ };
53
+
54
+ export function ActionButtonController({
55
+ action,
56
+ pluginIdentifier,
57
+ entry,
58
+ onMissingActionContext,
59
+ children,
60
+ }: Props) {
61
+ const resolvedEntry = entry;
62
+ const resolvedIdentifier = action?.identifier || pluginIdentifier || "";
63
+ const actionContext = useActions(resolvedIdentifier);
64
+
65
+ const actionDisabled =
66
+ typeof actionContext?.isActionAvailable === "function" &&
67
+ !actionContext.isActionAvailable(resolvedEntry);
68
+
69
+ const supportsEntryState =
70
+ typeof actionContext?.initialEntryState === "function" && !actionDisabled;
71
+
72
+ const hydrationKey = `${resolvedIdentifier}::${String(resolvedEntry?.id ?? "")}`;
73
+ const lastHydratedKeyRef = useRef<string | null>(null);
74
+
75
+ const [actionState, setActionState] = useState(() =>
76
+ supportsEntryState ? actionContext.initialEntryState(resolvedEntry) : null
77
+ );
78
+
79
+ useEffect(() => {
80
+ if (supportsEntryState) {
81
+ if (lastHydratedKeyRef.current === hydrationKey) {
82
+ return;
83
+ }
84
+
85
+ lastHydratedKeyRef.current = hydrationKey;
86
+ setActionState(actionContext.initialEntryState(resolvedEntry));
87
+ }
88
+ }, [supportsEntryState, actionContext, resolvedEntry, hydrationKey]);
89
+
90
+ useEffect(() => {
91
+ if (typeof actionContext?.addListener === "function") {
92
+ return actionContext.addListener(String(resolvedEntry?.id), (nextState) =>
93
+ setActionState(nextState)
94
+ );
95
+ }
96
+
97
+ if (typeof actionContext?.addListeners === "function") {
98
+ return actionContext.addListeners(
99
+ ({ entryState, entry: updatedEntry }) => {
100
+ if (updatedEntry?.id === resolvedEntry?.id) {
101
+ setActionState(entryState);
102
+ }
103
+ }
104
+ );
105
+ }
106
+
107
+ return undefined;
108
+ }, [
109
+ actionContext,
110
+ resolvedEntry?.id,
111
+ resolvedIdentifier,
112
+ actionContext?.addListener,
113
+ actionContext?.addListeners,
114
+ ]);
115
+
116
+ const legacySelected = useMemo(() => {
117
+ if (!actionContext || supportsEntryState) {
118
+ return false;
119
+ }
120
+
121
+ return buildLegacySelection(resolvedEntry, actionContext);
122
+ }, [actionContext, supportsEntryState, resolvedEntry]);
123
+
124
+ const isActive = resolveIsActive(actionState, legacySelected);
125
+
126
+ const onPress = useCallback(async () => {
127
+ if (!actionContext) {
128
+ return;
129
+ }
130
+
131
+ if (supportsEntryState) {
132
+ return actionContext.invokeAction?.(resolvedEntry, {
133
+ updateState: setActionState,
134
+ });
135
+ }
136
+
137
+ const favouritesAction = legacySelected
138
+ ? actionContext.removeFavourite
139
+ : actionContext.addFavourite;
140
+
141
+ const toggleAction = actionContext?.invokeAction ?? favouritesAction;
142
+
143
+ return toggleAction?.(resolvedEntry);
144
+ }, [actionContext, supportsEntryState, resolvedEntry, legacySelected]);
145
+
146
+ if (!actionContext || actionDisabled) {
147
+ if (!actionContext) {
148
+ onMissingActionContext?.(resolvedIdentifier);
149
+ }
150
+
151
+ return null;
152
+ }
153
+
154
+ return (
155
+ <>
156
+ {children({
157
+ actionContext,
158
+ actionState,
159
+ entry: resolvedEntry,
160
+ isActive,
161
+ onPress,
162
+ })}
163
+ </>
164
+ );
165
+ }
@@ -0,0 +1,405 @@
1
+ import React from "react";
2
+ import { Text } from "react-native";
3
+ import { render, fireEvent } from "@testing-library/react-native";
4
+ import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
+
6
+ import { ActionButtonController } from "../ActionButtonController";
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
+
14
+ const entry = { id: "entry-1" } as ZappEntry;
15
+
16
+ describe("ActionButtonController", () => {
17
+ beforeEach(() => {
18
+ jest.clearAllMocks();
19
+ });
20
+
21
+ it("returns null when action is unavailable", () => {
22
+ const actionContext = {
23
+ isActionAvailable: jest.fn(() => false),
24
+ initialEntryState: jest.fn(),
25
+ };
26
+
27
+ mockUseActions.mockReturnValue(actionContext);
28
+
29
+ const child = jest.fn(() => <Text testID="content">content</Text>);
30
+
31
+ const { queryByTestId } = render(
32
+ <ActionButtonController
33
+ action={{ identifier: "navigation_action" }}
34
+ entry={entry}
35
+ >
36
+ {child}
37
+ </ActionButtonController>
38
+ );
39
+
40
+ expect(queryByTestId("content")).toBeNull();
41
+ expect(child).not.toHaveBeenCalled();
42
+ expect(actionContext.isActionAvailable).toHaveBeenCalledWith(entry);
43
+ });
44
+
45
+ it("passes action context, action state and press handler to children", () => {
46
+ const actionContext = {
47
+ isActionAvailable: jest.fn(() => true),
48
+ initialEntryState: jest.fn(() => ({ active: true })),
49
+ invokeAction: jest.fn(),
50
+ addListener: jest.fn(() => jest.fn()),
51
+ };
52
+
53
+ mockUseActions.mockReturnValue(actionContext);
54
+
55
+ const child = jest.fn(({ actionState, onPress, isActive }) => (
56
+ <Text testID="content" onPress={onPress}>
57
+ {String(actionState?.active)}-{String(isActive)}
58
+ </Text>
59
+ ));
60
+
61
+ const { getByTestId } = render(
62
+ <ActionButtonController
63
+ action={{ identifier: "navigation_action" }}
64
+ entry={entry}
65
+ >
66
+ {child}
67
+ </ActionButtonController>
68
+ );
69
+
70
+ expect(actionContext.initialEntryState).toHaveBeenCalledWith(entry);
71
+
72
+ expect(child).toHaveBeenCalledWith(
73
+ expect.objectContaining({
74
+ actionContext,
75
+ actionState: { active: true },
76
+ entry,
77
+ isActive: true,
78
+ onPress: expect.any(Function),
79
+ })
80
+ );
81
+
82
+ fireEvent.press(getByTestId("content"));
83
+
84
+ expect(actionContext.invokeAction).toHaveBeenCalledWith(
85
+ entry,
86
+ expect.objectContaining({
87
+ updateState: expect.any(Function),
88
+ })
89
+ );
90
+ });
91
+
92
+ it("updates state from addListener", () => {
93
+ let listener;
94
+
95
+ const actionContext = {
96
+ isActionAvailable: jest.fn(() => true),
97
+ initialEntryState: jest.fn(() => ({ active: false })),
98
+ addListener: jest.fn((_entryId, callback) => {
99
+ listener = callback;
100
+
101
+ return jest.fn();
102
+ }),
103
+ invokeAction: jest.fn(),
104
+ };
105
+
106
+ mockUseActions.mockReturnValue(actionContext);
107
+
108
+ const { getByText } = render(
109
+ <ActionButtonController
110
+ action={{ identifier: "navigation_action" }}
111
+ entry={entry}
112
+ >
113
+ {({ actionState }) => <Text>{String(actionState?.active)}</Text>}
114
+ </ActionButtonController>
115
+ );
116
+
117
+ expect(getByText("false")).toBeTruthy();
118
+
119
+ listener({ active: true });
120
+
121
+ expect(getByText("true")).toBeTruthy();
122
+ });
123
+
124
+ it("subscribes via addListener and unsubscribes on unmount", () => {
125
+ const unsubscribe = jest.fn();
126
+
127
+ const actionContext = {
128
+ isActionAvailable: jest.fn(() => true),
129
+ initialEntryState: jest.fn(() => ({ active: false })),
130
+ addListener: jest.fn(() => unsubscribe),
131
+ };
132
+
133
+ mockUseActions.mockReturnValue(actionContext);
134
+
135
+ const { unmount } = render(
136
+ <ActionButtonController
137
+ action={{ identifier: "navigation_action" }}
138
+ entry={entry}
139
+ >
140
+ {() => <Text testID="content">content</Text>}
141
+ </ActionButtonController>
142
+ );
143
+
144
+ expect(actionContext.addListener).toHaveBeenCalledWith(
145
+ String(entry.id),
146
+ expect.any(Function)
147
+ );
148
+
149
+ unmount();
150
+
151
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
152
+ });
153
+
154
+ it("subscribes via addListeners and unsubscribes on unmount when addListener is missing", () => {
155
+ const unsubscribe = jest.fn();
156
+
157
+ const actionContext = {
158
+ isActionAvailable: jest.fn(() => true),
159
+ addListeners: jest.fn(() => unsubscribe),
160
+ initialEntryState: undefined,
161
+ };
162
+
163
+ mockUseActions.mockReturnValue(actionContext);
164
+
165
+ const { unmount } = render(
166
+ <ActionButtonController
167
+ action={{ identifier: "navigation_action" }}
168
+ entry={entry}
169
+ >
170
+ {() => <Text testID="content">content</Text>}
171
+ </ActionButtonController>
172
+ );
173
+
174
+ expect(actionContext.addListeners).toHaveBeenCalledWith(
175
+ expect.any(Function)
176
+ );
177
+
178
+ unmount();
179
+
180
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
181
+ });
182
+
183
+ it("subscribes when action context becomes available after initial render", () => {
184
+ const actionContext = {
185
+ isActionAvailable: jest.fn(() => true),
186
+ initialEntryState: jest.fn(() => ({ active: false })),
187
+ addListener: jest.fn(() => jest.fn()),
188
+ invokeAction: jest.fn(),
189
+ };
190
+
191
+ mockUseActions
192
+ .mockReturnValueOnce(undefined)
193
+ .mockReturnValue(actionContext);
194
+
195
+ const { rerender } = render(
196
+ <ActionButtonController
197
+ action={{ identifier: "navigation_action" }}
198
+ entry={entry}
199
+ >
200
+ {() => <Text testID="content">content</Text>}
201
+ </ActionButtonController>
202
+ );
203
+
204
+ expect(actionContext.addListener).not.toHaveBeenCalled();
205
+
206
+ rerender(
207
+ <ActionButtonController
208
+ action={{ identifier: "navigation_action" }}
209
+ entry={entry}
210
+ >
211
+ {() => <Text testID="content">content</Text>}
212
+ </ActionButtonController>
213
+ );
214
+
215
+ expect(actionContext.addListener).toHaveBeenCalledWith(
216
+ String(entry.id),
217
+ expect.any(Function)
218
+ );
219
+ });
220
+
221
+ it("falls back to legacy toggle behavior without initialEntryState", () => {
222
+ const addFavourite = jest.fn();
223
+ const removeFavourite = jest.fn();
224
+
225
+ const actionContext = {
226
+ state: [entry],
227
+ masterCell: {
228
+ isSelected: jest.fn(() => true),
229
+ },
230
+ addFavourite,
231
+ removeFavourite,
232
+ isActionAvailable: jest.fn(() => true),
233
+ addListeners: jest.fn(() => jest.fn()),
234
+ };
235
+
236
+ mockUseActions.mockReturnValue(actionContext);
237
+
238
+ const { getByTestId } = render(
239
+ <ActionButtonController
240
+ action={{ identifier: "navigation_action" }}
241
+ entry={entry}
242
+ >
243
+ {({ onPress, isActive }) => (
244
+ <Text testID="content" onPress={onPress}>
245
+ {String(isActive)}
246
+ </Text>
247
+ )}
248
+ </ActionButtonController>
249
+ );
250
+
251
+ expect(getByTestId("content").props.children).toBe("true");
252
+
253
+ fireEvent.press(getByTestId("content"));
254
+
255
+ expect(removeFavourite).toHaveBeenCalledWith(entry);
256
+ expect(addFavourite).not.toHaveBeenCalled();
257
+ });
258
+
259
+ it("uses legacy addFavourite when item is not selected", () => {
260
+ const addFavourite = jest.fn();
261
+ const removeFavourite = jest.fn();
262
+
263
+ const actionContext = {
264
+ state: [],
265
+ masterCell: {
266
+ isSelected: jest.fn(() => false),
267
+ },
268
+ addFavourite,
269
+ removeFavourite,
270
+ isActionAvailable: jest.fn(() => true),
271
+ addListeners: jest.fn(() => jest.fn()),
272
+ };
273
+
274
+ mockUseActions.mockReturnValue(actionContext);
275
+
276
+ const { getByTestId } = render(
277
+ <ActionButtonController
278
+ action={{ identifier: "navigation_action" }}
279
+ entry={entry}
280
+ >
281
+ {({ onPress, isActive }) => (
282
+ <Text testID="content" onPress={onPress}>
283
+ {String(isActive)}
284
+ </Text>
285
+ )}
286
+ </ActionButtonController>
287
+ );
288
+
289
+ expect(getByTestId("content").props.children).toBe("false");
290
+
291
+ fireEvent.press(getByTestId("content"));
292
+
293
+ expect(addFavourite).toHaveBeenCalledWith(entry);
294
+ expect(removeFavourite).not.toHaveBeenCalled();
295
+ });
296
+
297
+ it("prefers invokeAction over legacy favourites handlers in legacy mode", () => {
298
+ const invokeAction = jest.fn();
299
+ const addFavourite = jest.fn();
300
+ const removeFavourite = jest.fn();
301
+
302
+ const actionContext = {
303
+ state: [],
304
+ masterCell: {
305
+ isSelected: jest.fn(() => false),
306
+ },
307
+ invokeAction,
308
+ addFavourite,
309
+ removeFavourite,
310
+ isActionAvailable: jest.fn(() => true),
311
+ addListeners: jest.fn(() => jest.fn()),
312
+ };
313
+
314
+ mockUseActions.mockReturnValue(actionContext);
315
+
316
+ const { getByTestId } = render(
317
+ <ActionButtonController
318
+ action={{ identifier: "navigation_action" }}
319
+ entry={entry}
320
+ >
321
+ {({ onPress }) => (
322
+ <Text testID="content" onPress={onPress}>
323
+ press
324
+ </Text>
325
+ )}
326
+ </ActionButtonController>
327
+ );
328
+
329
+ fireEvent.press(getByTestId("content"));
330
+
331
+ expect(invokeAction).toHaveBeenCalledWith(entry);
332
+ expect(addFavourite).not.toHaveBeenCalled();
333
+ expect(removeFavourite).not.toHaveBeenCalled();
334
+ });
335
+
336
+ it("calls resolved legacy toggleAction onPress when invokeAction is missing", async () => {
337
+ const addFavourite = jest.fn(() => Promise.resolve("added"));
338
+ const removeFavourite = jest.fn();
339
+
340
+ const actionContext = {
341
+ state: [],
342
+ masterCell: {
343
+ isSelected: jest.fn(() => false),
344
+ },
345
+ addFavourite,
346
+ removeFavourite,
347
+ isActionAvailable: jest.fn(() => true),
348
+ addListeners: jest.fn(() => jest.fn()),
349
+ };
350
+
351
+ mockUseActions.mockReturnValue(actionContext);
352
+
353
+ let capturedOnPress: (() => Promise<unknown> | unknown) | undefined;
354
+
355
+ render(
356
+ <ActionButtonController
357
+ action={{ identifier: "navigation_action" }}
358
+ entry={entry}
359
+ >
360
+ {({ onPress }) => {
361
+ capturedOnPress = onPress;
362
+
363
+ return <Text testID="content">press</Text>;
364
+ }}
365
+ </ActionButtonController>
366
+ );
367
+
368
+ await capturedOnPress?.();
369
+
370
+ expect(addFavourite).toHaveBeenCalledWith(entry);
371
+ expect(removeFavourite).not.toHaveBeenCalled();
372
+ });
373
+
374
+ it("wires invokeAction updateState callback to update render state", () => {
375
+ const actionContext = {
376
+ isActionAvailable: jest.fn(() => true),
377
+ initialEntryState: jest.fn(() => ({ active: false })),
378
+ invokeAction: jest.fn((_entry, options) => {
379
+ options.updateState({ active: true });
380
+ }),
381
+ addListener: jest.fn(() => jest.fn()),
382
+ };
383
+
384
+ mockUseActions.mockReturnValue(actionContext);
385
+
386
+ const { getByTestId, getByText } = render(
387
+ <ActionButtonController
388
+ action={{ identifier: "navigation_action" }}
389
+ entry={entry}
390
+ >
391
+ {({ onPress, actionState }) => (
392
+ <Text testID="content" onPress={onPress}>
393
+ {String(actionState?.active)}
394
+ </Text>
395
+ )}
396
+ </ActionButtonController>
397
+ );
398
+
399
+ expect(getByText("false")).toBeTruthy();
400
+
401
+ fireEvent.press(getByTestId("content"));
402
+
403
+ expect(getByText("true")).toBeTruthy();
404
+ });
405
+ });
@@ -0,0 +1 @@
1
+ export { ActionButtonController } from "./ActionButtonController";
@@ -0,0 +1,47 @@
1
+ import {
2
+ getButtonSlotPrefix,
3
+ getEnabledButtonSlots,
4
+ getStylePrefix,
5
+ } from "./selectors";
6
+ import { buildContainerLayout } from "./style";
7
+ import { ActionButtonsModel, BuildActionButtonsModelOptions } from "./types";
8
+
9
+ export const buildActionButtonsModel = ({
10
+ configuration,
11
+ value,
12
+ containerPrefix,
13
+ buttonPrefix,
14
+ maxButtons = 3,
15
+ }: BuildActionButtonsModelOptions): ActionButtonsModel | null => {
16
+ if (!value(`${containerPrefix}_buttons_enabled`)) {
17
+ return null;
18
+ }
19
+
20
+ const enabledSlots = getEnabledButtonSlots(
21
+ configuration,
22
+ buttonPrefix,
23
+ maxButtons
24
+ );
25
+
26
+ if (enabledSlots.length === 0) {
27
+ return null;
28
+ }
29
+
30
+ const container = buildContainerLayout(value, containerPrefix);
31
+
32
+ return {
33
+ enabledSlots,
34
+ buttonsCount: enabledSlots.length,
35
+ container,
36
+ buttons: enabledSlots.map((slot, renderIndex) => ({
37
+ slot,
38
+ renderIndex,
39
+ specificPrefix: getButtonSlotPrefix(buttonPrefix, slot),
40
+ stylePrefix: getStylePrefix({
41
+ slot,
42
+ independentStyles: container.independentStyles,
43
+ buttonPrefix,
44
+ }),
45
+ })),
46
+ };
47
+ };