@applicaster/zapp-react-native-ui-components 15.0.0-alpha.5859164390 → 15.0.0-alpha.5904402522

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 (45) hide show
  1. package/Components/GeneralContentScreen/GeneralContentScreen.tsx +39 -28
  2. package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
  3. package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
  4. package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
  5. package/Components/HookRenderer/HookRenderer.tsx +40 -10
  6. package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
  7. package/Components/MasterCell/DefaultComponents/PressableView.tsx +196 -0
  8. package/Components/MasterCell/DefaultComponents/index.ts +2 -0
  9. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +46 -0
  10. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +124 -0
  11. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ButtonContainerView.ts +23 -0
  12. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  13. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  14. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +32 -0
  15. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +191 -0
  16. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +140 -0
  17. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +146 -0
  18. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +93 -0
  19. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +142 -0
  20. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +74 -0
  21. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +127 -0
  22. package/Components/MasterCell/elementMapper.tsx +1 -0
  23. package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
  24. package/Components/PreloaderWrapper/index.tsx +15 -0
  25. package/Components/River/RefreshControl.tsx +9 -3
  26. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
  27. package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
  28. package/Components/ScreenFeedLoader/index.ts +1 -0
  29. package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
  30. package/Components/ScreenResolver/hooks/index.ts +3 -0
  31. package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
  32. package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
  33. package/Components/ScreenResolver/index.tsx +14 -120
  34. package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
  35. package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
  36. package/Components/ScreenResolver/utils/index.ts +1 -0
  37. package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
  38. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
  39. package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
  40. package/Components/ScreenResolverFeedProvider/index.ts +1 -0
  41. package/Components/index.js +1 -1
  42. package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
  43. package/Contexts/ScreenContext/index.tsx +17 -0
  44. package/package.json +5 -5
  45. /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
@@ -12,12 +12,26 @@ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
12
12
  import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
13
13
  import { ScreenTrackedViewPositionsContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenTrackedViewPositionsContext";
14
14
  import { useEventAlerts } from "./utils/useEventAlerts";
15
-
16
- const { log_info } = createLogger({
15
+ import {
16
+ selectRiverById,
17
+ useAppSelector,
18
+ } from "@applicaster/zapp-react-native-redux";
19
+ import { getScreenDataSource } from "./utils/getScreenDataSource";
20
+ import { ScreenResolverFeedProvider } from "../ScreenResolverFeedProvider/ScreenResolverFeedProvider";
21
+
22
+ const { log_debug } = createLogger({
17
23
  category: "ScreenContainer",
18
24
  subsystem: "General",
19
25
  });
20
26
 
27
+ /** Provides screen-feed from general-screen configuration (if defined) */
28
+ const useFeedData = (id) => {
29
+ const river = useAppSelector((state) => selectRiverById(state, id));
30
+ const feedData = getScreenDataSource(river);
31
+
32
+ return feedData;
33
+ };
34
+
21
35
  export const GeneralContentScreen = ({
22
36
  feed,
23
37
  screenId,
@@ -54,20 +68,15 @@ export const GeneralContentScreen = ({
54
68
  useEffect(() => {
55
69
  if (!riverActionProvidersReady) {
56
70
  if (actionsInitialStateSetters.length > 0) {
57
- log_info(
58
- "ScreenContainer: starting to check river action providers to initialize",
59
- { actionsInitialStateSetters }
60
- );
61
-
62
71
  allSettled(actionsInitialStateSetters).finally(() => {
63
- log_info(
72
+ log_debug(
64
73
  "ScreenContainer: action provider ready, completed. Starting to present screen"
65
74
  );
66
75
 
67
76
  setRiverActionProvidersReady(true);
68
77
  });
69
78
  } else {
70
- log_info(
79
+ log_debug(
71
80
  "ScreenContainer: no action provider to check, completed. Starting to present screen"
72
81
  );
73
82
 
@@ -108,24 +117,26 @@ export const GeneralContentScreen = ({
108
117
  if (!isReady || isNilOrEmpty(components || uiComponents)) return null;
109
118
 
110
119
  return (
111
- <ScreenTrackedViewPositionsContext.Provider>
112
- <CellTapContext.Provider value={contextValue}>
113
- <ComponentsMap
114
- feed={feed}
115
- riverId={screenId}
116
- groupId={groupId || `general-content-screen-${screenId}`}
117
- riverComponents={components || uiComponents}
118
- scrollViewExtraProps={scrollViewExtraProps}
119
- getStaticComponentFeed={getStaticComponentFeed}
120
- extraAnchorPointYOffset={extraAnchorPointYOffset}
121
- isScreenWrappedInContainer={isScreenWrappedInContainer}
122
- parentFocus={parentFocus}
123
- focused={focused}
124
- containerHeight={containerHeight}
125
- preferredFocus={preferredFocus}
126
- {...componentsMapExtraProps}
127
- />
128
- </CellTapContext.Provider>
129
- </ScreenTrackedViewPositionsContext.Provider>
120
+ <ScreenResolverFeedProvider id={screenId} useFeedData={useFeedData}>
121
+ <ScreenTrackedViewPositionsContext.Provider>
122
+ <CellTapContext.Provider value={contextValue}>
123
+ <ComponentsMap
124
+ feed={feed}
125
+ riverId={screenId}
126
+ groupId={groupId || `general-content-screen-${screenId}`}
127
+ riverComponents={components || uiComponents}
128
+ scrollViewExtraProps={scrollViewExtraProps}
129
+ getStaticComponentFeed={getStaticComponentFeed}
130
+ extraAnchorPointYOffset={extraAnchorPointYOffset}
131
+ isScreenWrappedInContainer={isScreenWrappedInContainer}
132
+ parentFocus={parentFocus}
133
+ focused={focused}
134
+ containerHeight={containerHeight}
135
+ preferredFocus={preferredFocus}
136
+ {...componentsMapExtraProps}
137
+ />
138
+ </CellTapContext.Provider>
139
+ </ScreenTrackedViewPositionsContext.Provider>
140
+ </ScreenResolverFeedProvider>
130
141
  );
131
142
  };
@@ -0,0 +1,104 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { GeneralContentScreen } from "../GeneralContentScreen";
4
+
5
+ const mockUseAppSelector = jest.fn();
6
+ const mockSelectRiverById = jest.fn();
7
+ const mockProviderSpy = jest.fn();
8
+
9
+ jest.mock("../../River/ComponentsMap", () => ({
10
+ ComponentsMap: () => {
11
+ const React = require("react");
12
+ const { View } = require("react-native");
13
+
14
+ return <View testID="components-map" />;
15
+ },
16
+ }));
17
+
18
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
19
+ useActions: jest.fn(() => jest.fn()),
20
+ }));
21
+
22
+ jest.mock("../utils", () => ({
23
+ logger: { warn: jest.fn() },
24
+ whenMatchingType: jest.fn((_type, value) => value),
25
+ }));
26
+
27
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/layout", () => ({
28
+ useLayoutVersion: jest.fn(() => false),
29
+ }));
30
+
31
+ jest.mock(
32
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenData",
33
+ () => ({
34
+ useScreenData: jest.fn(() => ({
35
+ ui_components: [{ id: "ui-component" }],
36
+ general: {},
37
+ })),
38
+ })
39
+ );
40
+
41
+ jest.mock("../utils/useCurationAPI", () => ({
42
+ useCurationAPI: jest.fn(() => [{ id: "curation-component" }]),
43
+ }));
44
+
45
+ jest.mock("@applicaster/quick-brick-core/App/ActionSetters", () => ({
46
+ useRiverInitialState: jest.fn(() => []),
47
+ }));
48
+
49
+ jest.mock("../utils/useEventAlerts", () => ({
50
+ useEventAlerts: jest.fn(),
51
+ }));
52
+
53
+ jest.mock("@applicaster/zapp-react-native-redux", () => ({
54
+ useAppSelector: (...args) => mockUseAppSelector(...args),
55
+ selectRiverById: (...args) => mockSelectRiverById(...args),
56
+ }));
57
+
58
+ jest.mock("../utils/getScreenDataSource", () => ({
59
+ getScreenDataSource: jest.fn(() => ({
60
+ source: "https://feed",
61
+ mapping: {},
62
+ })),
63
+ }));
64
+
65
+ jest.mock(
66
+ "../../ScreenResolverFeedProvider/ScreenResolverFeedProvider",
67
+ () => ({
68
+ ScreenResolverFeedProvider: ({ id, useFeedData, children }) => {
69
+ const React = require("react");
70
+ const { View } = require("react-native");
71
+
72
+ mockProviderSpy(id, useFeedData);
73
+ useFeedData(id);
74
+
75
+ return <View testID="screen-resolver-feed-provider">{children}</View>;
76
+ },
77
+ })
78
+ );
79
+
80
+ describe("GeneralContentScreen", () => {
81
+ beforeEach(() => {
82
+ jest.clearAllMocks();
83
+ mockUseAppSelector.mockImplementation((selector) => selector({}));
84
+ mockSelectRiverById.mockReturnValue({ id: "screen-1" });
85
+ });
86
+
87
+ it("wraps content with ScreenResolverFeedProvider and renders ComponentsMap", () => {
88
+ const { getByTestId } = render(
89
+ <GeneralContentScreen
90
+ screenId="screen-1"
91
+ feed={null}
92
+ components={[{ id: "component-1" }]}
93
+ />
94
+ );
95
+
96
+ expect(getByTestId("screen-resolver-feed-provider")).toBeDefined();
97
+ expect(getByTestId("components-map")).toBeDefined();
98
+
99
+ expect(mockProviderSpy).toHaveBeenCalledWith(
100
+ "screen-1",
101
+ expect.any(Function)
102
+ );
103
+ });
104
+ });
@@ -0,0 +1,19 @@
1
+ import { getScreenDataSource } from "../getScreenDataSource";
2
+
3
+ describe("getScreenDataSource", () => {
4
+ it("returns screen_feed data when present", () => {
5
+ const result = getScreenDataSource({
6
+ data: {
7
+ screen_feed: {
8
+ source: "https://feed",
9
+ },
10
+ },
11
+ });
12
+
13
+ expect(result).toEqual({ source: "https://feed" });
14
+ });
15
+
16
+ it("returns undefined when screen_feed is missing", () => {
17
+ expect(getScreenDataSource({ data: {} })).toBeUndefined();
18
+ });
19
+ });
@@ -0,0 +1,9 @@
1
+ import { get } from "@applicaster/zapp-react-native-utils/utils";
2
+
3
+ const lookupPath = ["data", "screen_feed"];
4
+
5
+ export const getScreenDataSource = (
6
+ screenData: any
7
+ ): Option<ZappDataSource> => {
8
+ return get(screenData, lookupPath) as ZappDataSource | undefined;
9
+ };
@@ -6,7 +6,12 @@ import {
6
6
  } from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
7
7
  import { useHookAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks";
8
8
  import { useSetNavbarState } from "@applicaster/zapp-react-native-utils/reactHooks";
9
- import { PresentationType } from "../ScreenResolver";
9
+
10
+ import { componentsLogger } from "../../Helpers/logger";
11
+
12
+ const logger = componentsLogger.addSubsystem("HookRenderer");
13
+
14
+ const HOOK_PRESENTATION_TYPE = "Hook";
10
15
 
11
16
  type Props = {
12
17
  focused?: boolean;
@@ -15,20 +20,17 @@ type Props = {
15
20
  callback: hookCallback;
16
21
  };
17
22
 
18
- export const HookRenderer = (props: Props) => {
19
- const {
20
- focused,
21
- screenData: { payload, hookPlugin },
22
- callback,
23
- } = props;
24
-
25
- const { setVisible: showNavBar } = useSetNavbarState();
23
+ const HookRenderer = (props: Props) => {
24
+ const { focused, screenData, callback } = props;
25
+ const { payload, hookPlugin } = screenData;
26
26
 
27
27
  const {
28
28
  module: { Component: HookComponent, presentFullScreen },
29
29
  configuration,
30
30
  } = hookPlugin;
31
31
 
32
+ const { setVisible: showNavBar } = useSetNavbarState();
33
+
32
34
  useHookAnalytics(props);
33
35
 
34
36
  const isNavBarVisible = useIsNavBarVisible();
@@ -63,8 +65,36 @@ export const HookRenderer = (props: Props) => {
63
65
  hookPlugin,
64
66
  focused,
65
67
  parentFocus,
66
- presentationType: PresentationType.Hook,
68
+ presentationType: HOOK_PRESENTATION_TYPE,
67
69
  }}
68
70
  />
69
71
  );
70
72
  };
73
+
74
+ /**
75
+ * Guard component to prevent rendering HookRenderer when screenData or hookPlugin is missing. This is to avoid potential crashes due to missing data.
76
+ */
77
+ const HookRendererGuard = (props: Props) => {
78
+ React.useEffect(() => {
79
+ if (!props.screenData) {
80
+ logger.error(
81
+ "HookRenderer received no screenData, screen cannot be rendered"
82
+ );
83
+ } else if (!props.screenData.hookPlugin) {
84
+ logger.error(
85
+ "HookRenderer received screenData with no hookPlugin, screen cannot be rendered",
86
+ {
87
+ screenData: props.screenData,
88
+ }
89
+ );
90
+ }
91
+ }, [props.screenData]);
92
+
93
+ if (!props.screenData || !props.screenData.hookPlugin) {
94
+ return null;
95
+ }
96
+
97
+ return <HookRenderer {...props} />;
98
+ };
99
+
100
+ export { HookRendererGuard as HookRenderer };
@@ -0,0 +1,60 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { render } from "@testing-library/react-native";
4
+ import { HookRenderer } from "..";
5
+
6
+ jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
7
+ isTV: jest.fn(() => false),
8
+ }));
9
+
10
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation", () => ({
11
+ useBackHandler: jest.fn(),
12
+ useIsNavBarVisible: jest.fn(() => true),
13
+ }));
14
+
15
+ jest.mock(
16
+ "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks",
17
+ () => ({
18
+ useHookAnalytics: jest.fn(),
19
+ })
20
+ );
21
+
22
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
23
+ useSetNavbarState: jest.fn(() => ({
24
+ setVisible: jest.fn(),
25
+ })),
26
+ }));
27
+
28
+ describe("HookRenderer", () => {
29
+ it("returns null when hookPlugin is missing", () => {
30
+ const { toJSON } = render(
31
+ <HookRenderer callback={jest.fn()} screenData={{ payload: {} } as any} />
32
+ );
33
+
34
+ expect(toJSON()).toBeNull();
35
+ });
36
+
37
+ it("passes Hook presentationType to rendered hook component", () => {
38
+ const HookComponent = (props) => (
39
+ <View testID="hook-component" {...props} />
40
+ );
41
+
42
+ const { getByTestId } = render(
43
+ <HookRenderer
44
+ callback={jest.fn()}
45
+ screenData={{
46
+ payload: { foo: "bar" },
47
+ hookPlugin: {
48
+ module: {
49
+ Component: HookComponent,
50
+ presentFullScreen: false,
51
+ },
52
+ configuration: {},
53
+ },
54
+ }}
55
+ />
56
+ );
57
+
58
+ expect(getByTestId("hook-component").props.presentationType).toBe("Hook");
59
+ });
60
+ });
@@ -0,0 +1,196 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { TouchableOpacity } from "react-native";
3
+
4
+ import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
+
6
+ import { masterCellLogger } from "../logger";
7
+ import {
8
+ buildLegacySelection,
9
+ resolveIsActive,
10
+ resolveLabelText,
11
+ } from "./mobile/MobileActionButtons/helpers";
12
+
13
+ type ChildElementProps = {
14
+ children?: React.ReactNode;
15
+ mobileActionRole?: string;
16
+ state?: string;
17
+ [key: string]: unknown;
18
+ };
19
+
20
+ type Props = {
21
+ children?: React.ReactNode;
22
+ item: ZappEntry | ZappFeed;
23
+ action?: {
24
+ identifier?: string;
25
+ };
26
+ style?: Record<string, unknown>;
27
+ focusedStyles?: Record<string, unknown>;
28
+ testID?: string;
29
+ accessibilityLabel?: string;
30
+ accessibilityHint?: string;
31
+ };
32
+
33
+ const isValidElement = (
34
+ child: React.ReactNode
35
+ ): child is React.ReactElement<ChildElementProps> =>
36
+ React.isValidElement(child);
37
+
38
+ export function PressableView({
39
+ children,
40
+ item,
41
+ action,
42
+ style = {},
43
+ focusedStyles = {},
44
+ testID,
45
+ accessibilityLabel,
46
+ accessibilityHint,
47
+ }: Props) {
48
+ const actionContext = useActions(action?.identifier);
49
+
50
+ const actionDisabled =
51
+ typeof actionContext?.isActionAvailable === "function" &&
52
+ !actionContext.isActionAvailable(item);
53
+
54
+ const supportsEntryState =
55
+ typeof actionContext?.initialEntryState === "function" && !actionDisabled;
56
+
57
+ const [actionState, setActionState] = useState(() =>
58
+ supportsEntryState ? actionContext.initialEntryState(item) : null
59
+ );
60
+
61
+ useEffect(() => {
62
+ if (supportsEntryState) {
63
+ setActionState(actionContext.initialEntryState(item));
64
+ }
65
+ }, [supportsEntryState, item, actionContext]);
66
+
67
+ useEffect(() => {
68
+ if (typeof actionContext?.addListener === "function") {
69
+ return actionContext.addListener(String(item?.id), (nextState) => {
70
+ setActionState(nextState);
71
+ });
72
+ }
73
+
74
+ if (typeof actionContext?.addListeners === "function") {
75
+ return actionContext.addListeners(({ entryState, entry }) => {
76
+ if (entry?.id === item?.id) {
77
+ setActionState(entryState);
78
+ }
79
+ });
80
+ }
81
+
82
+ return undefined;
83
+ }, [actionContext, item?.id]);
84
+
85
+ const legacySelected = useMemo(() => {
86
+ if (!actionContext || supportsEntryState) {
87
+ return false;
88
+ }
89
+
90
+ return buildLegacySelection(item, actionContext);
91
+ }, [actionContext, supportsEntryState, item]);
92
+
93
+ const isActive = resolveIsActive(actionState, legacySelected);
94
+
95
+ const labelText = supportsEntryState
96
+ ? resolveLabelText(actionState?.label)
97
+ : "";
98
+
99
+ const shouldRenderAsset = Boolean(
100
+ supportsEntryState ? actionState?.asset : false
101
+ );
102
+
103
+ const shouldRenderLabel = Boolean(labelText);
104
+
105
+ const cloneChildrenWithState = (nodes?: React.ReactNode): React.ReactNode => {
106
+ return React.Children.map(nodes, (child) => {
107
+ if (!isValidElement(child)) {
108
+ return child;
109
+ }
110
+
111
+ const role = child.props.mobileActionRole;
112
+ const nextChildren = cloneChildrenWithState(child.props.children);
113
+
114
+ if (role === "asset" && !shouldRenderAsset) {
115
+ return null;
116
+ }
117
+
118
+ if (role === "label" && !shouldRenderLabel) {
119
+ return null;
120
+ }
121
+
122
+ if (
123
+ role === "label_container" &&
124
+ React.Children.count(nextChildren) === 0
125
+ ) {
126
+ return null;
127
+ }
128
+
129
+ const nextProps: Partial<ChildElementProps> = {};
130
+
131
+ if (role === "asset") {
132
+ nextProps.state = isActive ? "active" : "inactive";
133
+ }
134
+
135
+ if (role === "label") {
136
+ nextProps.state = isActive ? "focused" : "default";
137
+ }
138
+
139
+ if (nextChildren !== child.props.children) {
140
+ nextProps.children = nextChildren;
141
+ }
142
+
143
+ return React.cloneElement(child, nextProps);
144
+ });
145
+ };
146
+
147
+ const onPress = useCallback(async () => {
148
+ if (!actionContext) {
149
+ return;
150
+ }
151
+
152
+ if (supportsEntryState) {
153
+ return actionContext.invokeAction(item, {
154
+ updateState: setActionState,
155
+ });
156
+ }
157
+
158
+ const favouritesAction = legacySelected
159
+ ? actionContext.removeFavourite
160
+ : actionContext.addFavourite;
161
+
162
+ const toggleAction = actionContext?.invokeAction ?? favouritesAction;
163
+
164
+ return toggleAction?.(item);
165
+ }, [actionContext, supportsEntryState, item, legacySelected]);
166
+
167
+ if (!actionContext) {
168
+ masterCellLogger.warning(
169
+ `You're missing an action plugin(${action?.identifier}) required by your mobile action button.`
170
+ );
171
+
172
+ return null;
173
+ }
174
+
175
+ if (actionDisabled) {
176
+ return null;
177
+ }
178
+
179
+ if (!shouldRenderAsset && !shouldRenderLabel) {
180
+ return null;
181
+ }
182
+
183
+ return (
184
+ <TouchableOpacity
185
+ activeOpacity={1}
186
+ onPress={onPress}
187
+ testID={testID || `${item?.id}`}
188
+ accessibilityLabel={accessibilityLabel || `${item?.id}`}
189
+ accessibilityHint={accessibilityHint}
190
+ accessible={!!(testID || accessibilityLabel)}
191
+ style={isActive ? { ...style, ...focusedStyles } : style}
192
+ >
193
+ {cloneChildrenWithState(children)}
194
+ </TouchableOpacity>
195
+ );
196
+ }
@@ -16,12 +16,14 @@ import { CellBadge } from "./CellBadge";
16
16
  import { TvActionButtons } from "./tv/TvActionButtons";
17
17
  import { ButtonContainerView } from "./tv/ButtonContainerView";
18
18
  import { ImageBorderContainer } from "./ImageBorderContainer";
19
+ import { PressableView } from "./PressableView";
19
20
 
20
21
  export const defaultComponents = {
21
22
  View,
22
23
  CollapsibleTextContainer,
23
24
  Text,
24
25
  Button,
26
+ PressableView,
25
27
  Image,
26
28
  PureImage,
27
29
  ButtonContainerView,
@@ -0,0 +1,46 @@
1
+ import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
2
+
3
+ type Props = {
4
+ prefix: string;
5
+ value: Function;
6
+ actionIdentifier: string;
7
+ testID?: string;
8
+ };
9
+
10
+ export const Asset = ({ prefix, value, actionIdentifier, testID }: Props) => {
11
+ if (!value(`${prefix}_asset_enabled`)) {
12
+ return null;
13
+ }
14
+
15
+ return {
16
+ type: "Image",
17
+ style: {
18
+ width: toNumberWithDefaultZero(value(`${prefix}_asset_width`)),
19
+ height: toNumberWithDefaultZero(value(`${prefix}_asset_height`)),
20
+ marginTop: toNumberWithDefaultZero(value(`${prefix}_asset_margin_top`)),
21
+ marginRight: toNumberWithDefaultZero(
22
+ value(`${prefix}_asset_margin_right`)
23
+ ),
24
+ marginBottom: toNumberWithDefaultZero(
25
+ value(`${prefix}_asset_margin_bottom`)
26
+ ),
27
+ marginLeft: toNumberWithDefaultZero(value(`${prefix}_asset_margin_left`)),
28
+ backgroundColor: "transparent",
29
+ },
30
+ data: [
31
+ {
32
+ func: (entry) => entry,
33
+ args: [],
34
+ propName: "entry",
35
+ },
36
+ ],
37
+ additionalProps: {
38
+ source: {
39
+ context: actionIdentifier,
40
+ },
41
+ state: "inactive",
42
+ mobileActionRole: "asset",
43
+ testID: testID ? `${testID}-asset` : undefined,
44
+ },
45
+ };
46
+ };