@applicaster/zapp-react-native-utils 15.1.13-alpha.1034774223 → 15.1.13

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 (68) hide show
  1. package/actionsExecutor/ActionExecutor.ts +2 -40
  2. package/actionsExecutor/ActionExecutorContext.tsx +16 -70
  3. package/actionsExecutor/actions/index.ts +0 -4
  4. package/actionsExecutor/actions/refreshComponent.ts +0 -6
  5. package/actionsExecutor/actions/sendCloudEvent.ts +1 -6
  6. package/actionsExecutor/consts.ts +0 -23
  7. package/actionsExecutor/types.ts +1 -1
  8. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +2 -5
  9. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +2 -55
  10. package/appUtils/playerManager/OverlayObserver/utils.ts +20 -51
  11. package/arrayUtils/index.ts +0 -79
  12. package/configurationUtils/__tests__/manifestKeyParser.test.ts +0 -21
  13. package/configurationUtils/manifestKeyParser.ts +10 -50
  14. package/manifestUtils/defaultManifestConfigurations/player.js +1 -22
  15. package/manifestUtils/index.js +0 -3
  16. package/modalState/__tests__/index.test.ts +1 -30
  17. package/modalState/index.ts +83 -35
  18. package/package.json +2 -2
  19. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +54 -0
  20. package/playerUtils/getPlayerActionButtons.ts +17 -0
  21. package/playerUtils/index.ts +1 -17
  22. package/reactHooks/actions/index.ts +2 -67
  23. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  24. package/reactHooks/cell-click/index.ts +21 -14
  25. package/reactHooks/usePluginConfiguration.ts +3 -6
  26. package/uiActionsRegistrator/index.ts +203 -25
  27. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +0 -25
  28. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +0 -88
  29. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +0 -76
  30. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +0 -212
  31. package/actionsExecutor/actions/dismissBottomSheet.ts +0 -23
  32. package/actionsExecutor/actions/openBottomSheet.ts +0 -325
  33. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +0 -4
  34. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +0 -4
  35. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +0 -11
  36. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +0 -4
  37. package/arrayUtils/__tests__/reorderByIds.test.ts +0 -50
  38. package/configurationUtils/__tests__/modalBlocksParser.test.ts +0 -260
  39. package/configurationUtils/modalBlocksParser.ts +0 -296
  40. package/entryActions/__tests__/buildEntryActions.test.ts +0 -156
  41. package/entryActions/aliasPresentation.ts +0 -124
  42. package/entryActions/buildEntryActions.ts +0 -166
  43. package/entryActions/index.ts +0 -17
  44. package/entryActions/types.ts +0 -39
  45. package/manifestUtils/modalBlocks.js +0 -304
  46. package/modalState/ContentViewModel.ts +0 -113
  47. package/modalState/EditableCollection.ts +0 -17
  48. package/modalState/EditableCollectionRegistry.ts +0 -51
  49. package/modalState/ModalOrchestrator.ts +0 -211
  50. package/modalState/RemoteEditableCollection.ts +0 -227
  51. package/modalState/__tests__/ContentViewModel.editable.test.ts +0 -69
  52. package/modalState/__tests__/ContentViewModel.test.ts +0 -165
  53. package/modalState/__tests__/EditableCollectionRegistry.test.ts +0 -112
  54. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +0 -47
  55. package/modalState/__tests__/RemoteEditableCollection.test.ts +0 -326
  56. package/modalState/__tests__/useBottomSheetContent.test.ts +0 -349
  57. package/modalState/store.ts +0 -102
  58. package/modalState/types.ts +0 -159
  59. package/modalState/useBottomSheetContent.ts +0 -104
  60. package/playerUtils/__tests__/resolvePlayerActions.test.ts +0 -138
  61. package/playerUtils/resolvePlayerActions.ts +0 -71
  62. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +0 -93
  63. package/uiActionsRegistrator/__tests__/subscribe.test.ts +0 -107
  64. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +0 -52
  65. package/uiActionsRegistrator/registry.ts +0 -335
  66. package/uiActionsRegistrator/resolveActionIdentifiers.ts +0 -100
  67. package/uiActionsRegistrator/usableActionItems.ts +0 -66
  68. /package/reactHooks/actions/__tests__/{index.test.tsx → index.test.js} +0 -0
@@ -11,30 +11,17 @@ const currentPlatform = platformSelect({
11
11
  android_tv: "android",
12
12
  });
13
13
 
14
- // Do not change the order: focused_selected must be first, and "focus" must
15
- // come *after* "focused". Matching uses startsWith(state + "_"), so putting
16
- // "focus" first would steal focused_* / focused_selected_* keys. Adding
17
- // "focus" here affects every getAllSpecificStyles consumer (not only modals);
18
- // property names that themselves start with focus_ (e.g. focus_font_color as
19
- // a non-state style) would also be classified as the focus state.
20
- const states = [
21
- "focused_selected",
22
- "pressed",
23
- "focused",
24
- "focus",
25
- "selected",
26
- "default",
27
- ];
14
+ // Do not change the order, focused_selected should be first
15
+ const states = ["focused_selected", "pressed", "focused", "selected"];
28
16
 
29
17
  const platformSuffixes = ["ios", "tvos", "samsung", "lg", "android"];
30
- type StateKey = (typeof states)[number];
18
+ type StateKey = (typeof states)[number] | "default";
31
19
 
32
20
  export type GetAllSpecificStylesProps = {
33
21
  componentName: string;
34
22
  subComponentName: string;
35
23
  configuration: Record<string, any>;
36
24
  outStyles: Record<string, any>;
37
- styleSeparator?: string;
38
25
  };
39
26
 
40
27
  /*
@@ -48,23 +35,14 @@ export const getAllSpecificStyles = ({
48
35
  componentName,
49
36
  subComponentName,
50
37
  outStyles,
51
- styleSeparator = "_style_",
52
38
  }: GetAllSpecificStylesProps) => {
53
39
  if (!outStyles) throw new Error("outStyles is required");
54
40
  if (!componentName) throw new Error("componentName is required");
55
41
 
56
- const prefixes =
42
+ const prefix =
57
43
  subComponentName?.length > 0
58
- ? [
59
- styleSeparator
60
- ? `${componentName}${styleSeparator}${subComponentName}_`
61
- : `${subComponentName}_`,
62
- `${componentName}_style_${subComponentName}_`,
63
- ]
64
- : [
65
- styleSeparator ? `${componentName}${styleSeparator}` : "",
66
- `${componentName}_style_`,
67
- ];
44
+ ? `${componentName}_style_${subComponentName}_`
45
+ : `${componentName}_style_`;
68
46
 
69
47
  const defaultKey = "default";
70
48
 
@@ -73,24 +51,12 @@ export const getAllSpecificStyles = ({
73
51
  const tmpGenericDict: Record<StateKey, Record<string, any>> = {};
74
52
  const tmpPlatformDict: Record<StateKey, Record<string, any>> = {};
75
53
 
76
- const parseKey = (key: string, originalKey: string, value: any) => {
77
- const matchedPrefix = prefixes.find((p) => p && key.startsWith(p));
78
-
79
- if (!matchedPrefix) {
80
- return;
81
- }
82
-
83
- // Guard: for empty styleSeparator, if matching modern prefix, require dot in originalKey
84
- if (
85
- styleSeparator === "" &&
86
- matchedPrefix === `${subComponentName}_` &&
87
- !originalKey.includes(".")
88
- ) {
54
+ const parseKey = (key: string, value: any) => {
55
+ if (!key.startsWith(prefix)) {
89
56
  return;
90
57
  }
91
58
 
92
- let styleName = key.slice(matchedPrefix.length);
93
-
59
+ let styleName = key.slice(prefix.length);
94
60
  if (!styleName || styleName.startsWith("_")) return;
95
61
 
96
62
  const platform = platformSuffixes.find((prefix) =>
@@ -118,11 +84,6 @@ export const getAllSpecificStyles = ({
118
84
  state = defaultKey;
119
85
  }
120
86
 
121
- // Normalize Studio "focus" keys onto the runtime "focused" state bucket.
122
- if (state === "focus") {
123
- state = "focused";
124
- }
125
-
126
87
  if (!styleName) return;
127
88
 
128
89
  const camelCaseKey = toCamelCase(styleName);
@@ -132,8 +93,7 @@ export const getAllSpecificStyles = ({
132
93
  };
133
94
 
134
95
  for (const [key, value] of Object.entries(configuration)) {
135
- const formattedKey = key.replace(/\./g, "_");
136
- parseKey(formattedKey, key, value);
96
+ parseKey(key, value);
137
97
  }
138
98
 
139
99
  const allStates = Array.from(
@@ -204,20 +204,6 @@ function getPlayerConfiguration({ platform, version }) {
204
204
  label_tooltip: "Label for close button accessibility",
205
205
  type: "text_input",
206
206
  },
207
- {
208
- key: "accessibility_overflow_label",
209
- label: "Accessibility overflow label",
210
- initial_value: "More actions",
211
- label_tooltip: "Label for the overflow (…) button accessibility",
212
- type: "text_input",
213
- },
214
- {
215
- key: "accessibility_overflow_hint",
216
- label: "Accessibility overflow hint",
217
- initial_value: "Opens the remaining player actions",
218
- label_tooltip: "Hint for the overflow (…) button accessibility",
219
- type: "text_input",
220
- },
221
207
  {
222
208
  key: "accessibility_close_hint",
223
209
  label: "Accessibility close hint",
@@ -761,7 +747,7 @@ function getPlayerConfiguration({ platform, version }) {
761
747
  label: "Player action buttons",
762
748
  initial_value: null,
763
749
  label_tooltip:
764
- "Comma-separated action identifiers. The entry_actions token expands from the playing entry's extensions.entry_action. When more than 2 actions resolve, the video and fullscreen audio players show Overflow (…) plus one companion - the first action that did not come from the entry, otherwise the first one - and the Overflow sheet lists every resolved action. A repeated identifier is counted once. No extra plugin is required.",
750
+ "type here the identifiers of the action buttons you want to show on the player, separated by commas. Only 2 action buttons may be added at the same time",
765
751
  type: "text_input",
766
752
  },
767
753
  {
@@ -1061,13 +1047,6 @@ function getPlayerConfiguration({ platform, version }) {
1061
1047
  type: "uploader",
1062
1048
  label_tooltip: "Override default close badge / icon",
1063
1049
  },
1064
- {
1065
- key: "overflow",
1066
- label: "Overflow badge",
1067
- type: "uploader",
1068
- label_tooltip:
1069
- "Override default overflow (…) badge / icon, shown when more action buttons resolve than fit on the player overlay",
1070
- },
1071
1050
  {
1072
1051
  key: "lock",
1073
1052
  label: "Lock player badge",
@@ -23,7 +23,6 @@ const {
23
23
  const { buttonKey } = require("./buttons");
24
24
  const { tvBadges } = require("./tvBadges");
25
25
  const { mobileProgressBar, tvProgressBar } = require("./progressBar");
26
- const { getModalBlockStyles, getModalLocalizations } = require("./modalBlocks");
27
26
 
28
27
  const {
29
28
  secondaryImage,
@@ -53,6 +52,4 @@ module.exports = {
53
52
  getUpdatedSecondaryImageKeys,
54
53
  DEFAULT_GRADIENT_IMAGE,
55
54
  compact,
56
- getModalBlockStyles,
57
- getModalLocalizations,
58
55
  };
@@ -1,4 +1,4 @@
1
- import { modalStore, getMenuItemActions } from "../";
1
+ import { modalStore } from "../";
2
2
 
3
3
  describe("Modal State", () => {
4
4
  it("should have initial state", () => {
@@ -48,33 +48,4 @@ describe("Modal State", () => {
48
48
  });
49
49
  });
50
50
  });
51
-
52
- describe("getMenuItemActions", () => {
53
- it("returns actions array when actions is provided", () => {
54
- const actions = [{ type: "action1" }, { type: "action2" }];
55
- const item = { id: "1", title: "Item", actions };
56
-
57
- expect(getMenuItemActions(item)).toEqual(actions);
58
- });
59
-
60
- it("returns [action] when single action is provided", () => {
61
- const action = { type: "action1" };
62
- const item = { id: "1", title: "Item", action };
63
-
64
- expect(getMenuItemActions(item)).toEqual([action]);
65
- });
66
-
67
- it("returns action when action is passed as an array", () => {
68
- const actions = [{ type: "action1" }];
69
- const item = { id: "1", title: "Item", action: actions as any };
70
-
71
- expect(getMenuItemActions(item)).toEqual(actions);
72
- });
73
-
74
- it("returns empty array when no action or actions are provided", () => {
75
- expect(getMenuItemActions({ id: "1", title: "Item" })).toEqual([]);
76
- expect(getMenuItemActions(null)).toEqual([]);
77
- expect(getMenuItemActions(undefined)).toEqual([]);
78
- });
79
- });
80
51
  });
@@ -1,35 +1,83 @@
1
- export {
2
- modalStore,
3
- useModalStore,
4
- openModal,
5
- dismissModal,
6
- openBottomSheetModal,
7
- useModalStoreState,
8
- } from "./store";
9
-
10
- export type { ModalState, ModalStore } from "./store";
11
-
12
- export { modalOrchestrator } from "./ModalOrchestrator";
13
-
14
- export { ContentViewModel } from "./ContentViewModel";
15
-
16
- export type {
17
- Menu,
18
- MenuItem,
19
- Header,
20
- Content,
21
- Action,
22
- PresentSubMenuAction,
23
- SecondaryAction,
24
- SelectMode,
25
- SelectionBehavior,
26
- } from "./types";
27
-
28
- export {
29
- isPresentSubMenuAction,
30
- getMenuItemActions,
31
- OPERATIONS,
32
- hasOperation,
33
- } from "./types";
34
-
35
- export { useBottomSheetContent } from "./useBottomSheetContent";
1
+ import { useStore, createStore } from "zustand";
2
+
3
+ interface ModalState {
4
+ visible: boolean;
5
+ screen: any;
6
+ options: Record<string, unknown>;
7
+ props: Record<string, unknown>;
8
+ }
9
+
10
+ interface ModalStore {
11
+ modalState: ModalState;
12
+ setModalState: (newState: Partial<ModalState>) => void;
13
+ openModal: (
14
+ item: any,
15
+ options?: Record<string, unknown>,
16
+ props?: Record<string, unknown>
17
+ ) => void;
18
+ dismissModal: () => void;
19
+ }
20
+
21
+ const initialModalState: ModalState = {
22
+ visible: false,
23
+ screen: null,
24
+ options: {},
25
+ props: {},
26
+ };
27
+
28
+ export const modalStore = createStore<ModalStore>((set) => ({
29
+ modalState: initialModalState,
30
+ setModalState: (newState) =>
31
+ set((state) => ({
32
+ modalState: { ...state.modalState, ...newState },
33
+ })),
34
+ openModal: ({ item, options = {}, props }: OpenModalArg) =>
35
+ set({
36
+ modalState: {
37
+ visible: true,
38
+ screen: item,
39
+ options: {
40
+ animated: true,
41
+ animationType: "slide",
42
+ onShow: () => {},
43
+ presentationStyle: "overFullScreen",
44
+ transparent: true,
45
+ ...options,
46
+ onDismiss: () => set({ modalState: initialModalState }),
47
+ onRequestClose: () => set({ modalState: initialModalState }),
48
+ },
49
+ props,
50
+ },
51
+ }),
52
+ dismissModal: () => set(() => ({ modalState: initialModalState })),
53
+ }));
54
+
55
+ export const useModalStore = <T>(selector: (state: ModalStore) => T) =>
56
+ useStore(modalStore, selector);
57
+
58
+ export const openModal = modalStore.getInitialState().openModal;
59
+
60
+ export const dismissModal = modalStore.getInitialState().dismissModal;
61
+
62
+ export const openBottomSheetModal = ({
63
+ ModalBottomSheetContent,
64
+ modalBottomSheetContentProps,
65
+ props = {},
66
+ options = {},
67
+ }: OpenModalBottomSheetArgs) => {
68
+ openModal({
69
+ item: {
70
+ ModalBottomSheetContent,
71
+ modalBottomSheetContentProps,
72
+ },
73
+ props,
74
+ options: {
75
+ animationType: "none",
76
+ animated: false,
77
+ ...options,
78
+ },
79
+ });
80
+ };
81
+
82
+ export const useModalStoreState = (): ModalState =>
83
+ useModalStore<ModalState>((state) => state.modalState);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "15.1.13-alpha.1034774223",
3
+ "version": "15.1.13",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "15.1.13-alpha.1034774223",
30
+ "@applicaster/applicaster-types": "15.1.13",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -0,0 +1,54 @@
1
+ import { getPlayerActionButtons } from "../getPlayerActionButtons";
2
+ import { selectActionButtons } from "../../conf/player/selectors";
3
+
4
+ jest.mock("../../conf/player/selectors", () => ({
5
+ selectActionButtons: jest.fn(),
6
+ }));
7
+
8
+ describe("getPlayerActionButtons", () => {
9
+ afterEach(() => {
10
+ jest.clearAllMocks();
11
+ });
12
+
13
+ it("returns an empty array if selectActionButtons returns undefined", () => {
14
+ (selectActionButtons as jest.Mock).mockReturnValue(undefined);
15
+ const result = getPlayerActionButtons({});
16
+ expect(result).toEqual([]);
17
+ });
18
+
19
+ it("returns an empty array if selectActionButtons returns null", () => {
20
+ (selectActionButtons as jest.Mock).mockReturnValue(null);
21
+ const result = getPlayerActionButtons({});
22
+ expect(result).toEqual([]);
23
+ });
24
+
25
+ it("returns an empty array if selectActionButtons returns empty string", () => {
26
+ (selectActionButtons as jest.Mock).mockReturnValue("");
27
+ const result = getPlayerActionButtons({});
28
+ expect(result).toEqual([]);
29
+ });
30
+
31
+ it("returns the first two trimmed action buttons", () => {
32
+ (selectActionButtons as jest.Mock).mockReturnValue(" play , pause , stop ");
33
+ const result = getPlayerActionButtons({});
34
+ expect(result).toEqual(["play", "pause"]);
35
+ });
36
+
37
+ it("returns only one button if only one is present", () => {
38
+ (selectActionButtons as jest.Mock).mockReturnValue(" play ");
39
+ const result = getPlayerActionButtons({});
40
+ expect(result).toEqual(["play"]);
41
+ });
42
+
43
+ it("trims whitespace from button names", () => {
44
+ (selectActionButtons as jest.Mock).mockReturnValue(" play , pause ");
45
+ const result = getPlayerActionButtons({});
46
+ expect(result).toEqual(["play", "pause"]);
47
+ });
48
+
49
+ it("returns an empty array if selectActionButtons returns only commas", () => {
50
+ (selectActionButtons as jest.Mock).mockReturnValue(" , , ");
51
+ const result = getPlayerActionButtons({});
52
+ expect(result).toEqual(["", ""]);
53
+ });
54
+ });
@@ -0,0 +1,17 @@
1
+ import { map, take, trim } from "../utils";
2
+ import { selectActionButtons } from "../conf/player/selectors";
3
+
4
+ /**
5
+ * Returns the first two action buttons from the configuration.
6
+ * @param {Object} configuration - The player configuration object.
7
+ * @returns {Array} An array containing the first two action buttons.
8
+ */
9
+ export const getPlayerActionButtons = (configuration: any) => {
10
+ const buttonsString = selectActionButtons(configuration);
11
+
12
+ if (!buttonsString) {
13
+ return [];
14
+ }
15
+
16
+ return take(2, map(buttonsString.split(","), trim));
17
+ };
@@ -7,23 +7,7 @@ import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
7
7
  import { getBoolFromConfigValue } from "../configurationUtils";
8
8
  import { Dimensions } from "react-native";
9
9
 
10
- export { resolvePlayerActions } from "./resolvePlayerActions";
11
-
12
- export { isEntryAction } from "../entryActions";
13
-
14
- export {
15
- parseActionIdentifiers,
16
- resolveActionIdentifiers,
17
- } from "../uiActionsRegistrator";
18
-
19
- export type { ResolveActionIdentifiersParams } from "../uiActionsRegistrator";
20
-
21
- export type {
22
- ActionButtonDescriptor,
23
- OverflowDescriptor,
24
- PlayerControlDescriptor,
25
- ResolvePlayerActionsParams,
26
- } from "./resolvePlayerActions";
10
+ export { getPlayerActionButtons } from "./getPlayerActionButtons";
27
11
 
28
12
  export {
29
13
  resolvePlayerContentText,
@@ -6,7 +6,6 @@ import { ActionsContext } from "@applicaster/zapp-react-native-ui-components/Con
6
6
  import {
7
7
  observeEntryState,
8
8
  RegisteredActionValue,
9
- uiActionsRegistry,
10
9
  } from "../../uiActionsRegistrator";
11
10
  import { reactHooksLogger } from "../logger";
12
11
 
@@ -35,13 +34,7 @@ export function useActions<T = unknown>(plugId: string): any {
35
34
  if (!logged) {
36
35
  logger.warning({
37
36
  message: `useActions: Couldn't find an action for ${plugId} plugin`,
38
- // Identifiers only. The action map holds every plugin's module and
39
- // live React context, which is far too large for a log line and can
40
- // carry whatever state those plugins hold.
41
- data: {
42
- plugId,
43
- availableActions: Object.keys(context?.actions ?? {}),
44
- },
37
+ data: { actions: context?.actions },
45
38
  });
46
39
 
47
40
  setLogged(true);
@@ -87,31 +80,7 @@ export function useEntryActionState(
87
80
  useEffect(() => {
88
81
  if (!action) return undefined;
89
82
 
90
- // An action's stream is third-party code. Subscribing with only a `next`
91
- // handler lets RxJS rethrow an error notification asynchronously, where no
92
- // error boundary catches it - one plugin faulting would take the app down
93
- // rather than this one button. The button keeps its last known state.
94
- let subscription;
95
-
96
- try {
97
- subscription = observeEntryState(action, entry).subscribe({
98
- next: setState,
99
- error: (error) =>
100
- logger.warning({
101
- message:
102
- "useEntryActionState: the action's state stream failed - the button keeps its last state",
103
- data: { entryId, error },
104
- }),
105
- });
106
- } catch (error) {
107
- logger.warning({
108
- message:
109
- "useEntryActionState: could not observe the action - the button keeps its initial state",
110
- data: { entryId, error },
111
- });
112
-
113
- return undefined;
114
- }
83
+ const subscription = observeEntryState(action, entry).subscribe(setState);
115
84
 
116
85
  return () => subscription.unsubscribe();
117
86
  // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
@@ -119,18 +88,6 @@ export function useEntryActionState(
119
88
 
120
89
  const invokeAction = useCallback(
121
90
  (options: InvokeArgsOptions = {}) => {
122
- if (typeof action?.invokeAction !== "function") {
123
- // A button was rendered and pressed, and the press goes nowhere. Only
124
- // reachable on a user interaction, so it costs nothing on render. The
125
- // action value carries no identifier, so the label is what locates the
126
- // button that misbehaved.
127
- logger.warning({
128
- message:
129
- "useEntryActionState: the pressed action has no invokeAction - the press did nothing",
130
- data: { entryId, label: state?.label },
131
- });
132
- }
133
-
134
91
  action?.invokeAction?.(entry, { updateState: setState, ...options });
135
92
  },
136
93
  // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
@@ -139,25 +96,3 @@ export function useEntryActionState(
139
96
 
140
97
  return { state, invokeAction };
141
98
  }
142
-
143
- /**
144
- * A value that changes whenever a provider is added to or removed from
145
- * `uiActionsRegistry`.
146
- *
147
- * Providers can register after the UI that wants them has rendered - the audio
148
- * player registers playback speed only once its controller has state, and
149
- * plugin-backed actions are bridged in from an effect. Put this in the
150
- * dependency list of anything that resolves actions, so the list is re-read
151
- * instead of being frozen at first paint.
152
- */
153
- export function useRegistryRevision(): number {
154
- const [revision, setRevision] = useState(0);
155
-
156
- useEffect(
157
- () =>
158
- uiActionsRegistry.subscribe(() => setRevision((current) => current + 1)),
159
- []
160
- );
161
-
162
- return revision;
163
- }
@@ -7,6 +7,7 @@ import { waitFor } from "@testing-library/react-native";
7
7
 
8
8
  const mockNavigator = {
9
9
  getPathname: () => "pathnameMock",
10
+ currentRoute: "pathnameMock",
10
11
  push: jest.fn(),
11
12
  data: {},
12
13
  };
@@ -31,6 +32,7 @@ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen", () => ({
31
32
  ),
32
33
  useTargetScreenData: jest.fn(() => ({})),
33
34
  useCurrentScreenData: jest.fn(() => ({})),
35
+ useScreenContext: jest.fn(() => ({})),
34
36
  }));
35
37
 
36
38
  jest.mock("@applicaster/zapp-react-native-ui-components/Contexts", () => ({
@@ -68,6 +70,12 @@ const wrapper = ({ children }) => (
68
70
  </CellTapContext.Provider>
69
71
  );
70
72
 
73
+ const actionExecutorWrapper = ({ children }) => (
74
+ <ActionExecutorContext.Provider value={actionExecutor}>
75
+ {children}
76
+ </ActionExecutorContext.Provider>
77
+ );
78
+
71
79
  describe("useCellClick", () => {
72
80
  it("returns a function", () => {
73
81
  const { result } = renderHook(() => useCellClick(mockProps));
@@ -97,4 +105,60 @@ describe("useCellClick", () => {
97
105
 
98
106
  cleanup();
99
107
  });
108
+
109
+ it("does not push when the cell finishes a hook", async () => {
110
+ mockNavigator.push.mockClear();
111
+
112
+ const item = {
113
+ id: "profile-1",
114
+ type: { value: "feed" },
115
+ extensions: {
116
+ tap_actions: {
117
+ actions: [
118
+ { type: "sessionStorageSet" },
119
+ { type: "finishHook", options: { success: true } },
120
+ ],
121
+ },
122
+ },
123
+ };
124
+
125
+ const { result } = renderHook(() => useCellClick({ item }), {
126
+ wrapper: actionExecutorWrapper,
127
+ });
128
+
129
+ await result.current();
130
+
131
+ await waitFor(() => {
132
+ expect(actionExecutor.handleEntryActions).toHaveBeenCalled();
133
+ });
134
+
135
+ expect(mockNavigator.push).not.toHaveBeenCalled();
136
+ cleanup();
137
+ });
138
+
139
+ it("pushes after tap actions that do not finish a hook", async () => {
140
+ mockNavigator.push.mockClear();
141
+
142
+ const item = {
143
+ id: "episode-1",
144
+ type: { value: "feed" },
145
+ extensions: {
146
+ tap_actions: {
147
+ actions: [{ type: "sessionStorageSet" }],
148
+ },
149
+ },
150
+ };
151
+
152
+ const { result } = renderHook(() => useCellClick({ item }), {
153
+ wrapper: actionExecutorWrapper,
154
+ });
155
+
156
+ await result.current();
157
+
158
+ await waitFor(() => {
159
+ expect(mockNavigator.push).toHaveBeenCalled();
160
+ });
161
+
162
+ cleanup();
163
+ });
100
164
  });
@@ -82,6 +82,12 @@ export const useCellClick = ({
82
82
 
83
83
  logOnPress(selectedItem, pathname, component, onCellTap);
84
84
 
85
+ const tapActions = selectedItem?.extensions?.tap_actions?.actions;
86
+
87
+ const finishesHook =
88
+ Array.isArray(tapActions) &&
89
+ tapActions.some((action) => action?.type === "finishHook");
90
+
85
91
  if (selectedItem) {
86
92
  await actionExecutor?.handleEntryActions(selectedItem, {
87
93
  component,
@@ -99,20 +105,21 @@ export const useCellClick = ({
99
105
  url: selectedItem.link.href,
100
106
  });
101
107
  }
102
- } else {
103
- if (isFunction(onCellTap)) {
104
- onCellTap?.(selectedItem, _index);
105
- } else {
106
- if (currentRoute === pathname || pathname.includes("video-modal")) {
107
- const targetScreen = component?.data?.target;
108
-
109
- push(
110
- R.when(
111
- () => targetScreen,
112
- R.mergeLeft({ screen_type: targetScreen })
113
- )(selectedItem)
114
- );
115
- }
108
+ } else if (isFunction(onCellTap)) {
109
+ onCellTap?.(selectedItem, _index);
110
+ } else if (!finishesHook) {
111
+ // finishHook already completes the hook and navigates to the original
112
+ // target. Pushing the cell as well stacks a second copy of that screen
113
+ // (e.g. home/home after a profile selector hook).
114
+ if (currentRoute === pathname || pathname.includes("video-modal")) {
115
+ const targetScreen = component?.data?.target;
116
+
117
+ push(
118
+ R.when(
119
+ () => targetScreen,
120
+ R.mergeLeft({ screen_type: targetScreen })
121
+ )(selectedItem)
122
+ );
116
123
  }
117
124
  }
118
125
  },