@applicaster/zapp-react-native-utils 15.1.6-alpha.3988508055 → 15.1.7-alpha.1530511842

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 (37) hide show
  1. package/actionUtils/index.ts +7 -0
  2. package/actionsExecutor/ActionExecutorContext.tsx +0 -4
  3. package/actionsExecutor/actions/__tests__/showToast.test.ts +29 -4
  4. package/actionsExecutor/actions/index.ts +0 -4
  5. package/actionsExecutor/actions/refreshComponent.ts +0 -6
  6. package/actionsExecutor/actions/showToast.ts +62 -6
  7. package/actionsExecutor/feedDecorator.ts +6 -6
  8. package/actionsExecutor/types.ts +3 -5
  9. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +41 -0
  10. package/appUtils/playerManager/OverlayObserver/utils.ts +3 -0
  11. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  12. package/manifestUtils/defaultManifestConfigurations/player.js +156 -0
  13. package/modalState/index.ts +83 -30
  14. package/package.json +2 -2
  15. package/reactHooks/index.ts +2 -0
  16. package/reactHooks/layout/index.ts +1 -1
  17. package/reactHooks/utils/__tests__/index.test.js +22 -2
  18. package/reactHooks/utils/index.ts +11 -1
  19. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +0 -22
  20. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +0 -88
  21. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +0 -76
  22. package/actionsExecutor/actions/dismissBottomSheet.ts +0 -22
  23. package/actionsExecutor/actions/openBottomSheet.ts +0 -318
  24. package/modalState/ContentViewModel.ts +0 -111
  25. package/modalState/EditableCollection.ts +0 -17
  26. package/modalState/EditableCollectionRegistry.ts +0 -51
  27. package/modalState/ModalOrchestrator.ts +0 -198
  28. package/modalState/RemoteEditableCollection.ts +0 -189
  29. package/modalState/__tests__/ContentViewModel.editable.test.ts +0 -69
  30. package/modalState/__tests__/ContentViewModel.test.ts +0 -165
  31. package/modalState/__tests__/EditableCollectionRegistry.test.ts +0 -112
  32. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +0 -28
  33. package/modalState/__tests__/RemoteEditableCollection.test.ts +0 -259
  34. package/modalState/__tests__/useBottomSheetContent.test.ts +0 -372
  35. package/modalState/store.ts +0 -102
  36. package/modalState/types.ts +0 -110
  37. package/modalState/useBottomSheetContent.ts +0 -98
@@ -0,0 +1,7 @@
1
+ import { Image } from "react-native";
2
+ import { isTV } from "../reactUtils";
3
+
4
+ export const resolveDefaultAssetUri = (source: string | number): string =>
5
+ isTV()
6
+ ? (source as string)
7
+ : Image.resolveAssetSource(source as number)?.uri || "";
@@ -19,11 +19,9 @@ import {
19
19
  appRestartAction,
20
20
  confirmDialogAction,
21
21
  createNavigateToScreenAction,
22
- dismissBottomSheetAction,
23
22
  localStorageRemoveAction,
24
23
  localStorageSetAction,
25
24
  localStorageToggleFlagAction,
26
- openBottomSheetAction,
27
25
  refreshComponentAction,
28
26
  screenSetVariableAction,
29
27
  screenToggleFlagAction,
@@ -92,8 +90,6 @@ const prepareDefaultActions = (actionExecutor) => {
92
90
 
93
91
  actionExecutor.registerAction("screenSetVariable", screenSetVariableAction);
94
92
  actionExecutor.registerAction("screenToggleFlag", screenToggleFlagAction);
95
- actionExecutor.registerAction("openBottomSheet", openBottomSheetAction);
96
- actionExecutor.registerAction("dismissBottomSheet", dismissBottomSheetAction);
97
93
  actionExecutor.registerAction("showToast", showToastAction);
98
94
  };
99
95
 
@@ -11,14 +11,13 @@ describe("showToastAction", () => {
11
11
  jest.clearAllMocks();
12
12
  });
13
13
 
14
- it("calls postEvent with showToast payload and returns ActionResult.Success", async () => {
14
+ it("calls postEvent with confirmation source, default timeout, and returns Success", async () => {
15
15
  const action = {
16
16
  type: "showToast",
17
17
  options: {
18
18
  id: "toast-1",
19
19
  message: "Added to queue",
20
20
  extraMessage: "Extra info",
21
- timeout: 3000,
22
21
  },
23
22
  };
24
23
 
@@ -32,13 +31,38 @@ describe("showToastAction", () => {
32
31
  message: "Added to queue",
33
32
  extraMessage: "Extra info",
34
33
  style: undefined,
35
- timeout: 3000,
34
+ timeout: 1000,
35
+ source: "confirmation",
36
36
  },
37
37
  ]);
38
38
 
39
39
  expect(result).toBe(ActionResult.Success);
40
40
  });
41
41
 
42
+ it("preserves an explicit timeout and style override", async () => {
43
+ const action = {
44
+ type: "showToast",
45
+ options: {
46
+ message: "Playlist created",
47
+ timeout: 3000,
48
+ style: { backgroundColor: "#00FF00" },
49
+ },
50
+ };
51
+
52
+ await showToastAction(action);
53
+
54
+ expect(postEvent).toHaveBeenCalledWith("showToast", [
55
+ {
56
+ id: undefined,
57
+ message: "Playlist created",
58
+ extraMessage: undefined,
59
+ style: { backgroundColor: "#00FF00" },
60
+ timeout: 3000,
61
+ source: "confirmation",
62
+ },
63
+ ]);
64
+ });
65
+
42
66
  it("handles missing or empty options without throwing", async () => {
43
67
  const action = {
44
68
  type: "showToast",
@@ -54,7 +78,8 @@ describe("showToastAction", () => {
54
78
  message: undefined,
55
79
  extraMessage: undefined,
56
80
  style: undefined,
57
- timeout: undefined,
81
+ timeout: 1000,
82
+ source: "confirmation",
58
83
  },
59
84
  ]);
60
85
 
@@ -28,7 +28,3 @@ export { screenToggleFlagAction } from "./screenToggleFlag";
28
28
  export { createNavigateToScreenAction } from "./navigateToScreen";
29
29
 
30
30
  export { showToastAction } from "./showToast";
31
-
32
- export { openBottomSheetAction } from "./openBottomSheet";
33
-
34
- export { dismissBottomSheetAction } from "./dismissBottomSheet";
@@ -82,12 +82,6 @@ export const refreshComponentAction: ActionHandler = async (
82
82
  }) || source;
83
83
  }
84
84
 
85
- if (!dataSource) {
86
- log_info("handleAction: refreshComponent skipped: no dataSource found");
87
-
88
- return ActionResult.Success;
89
- }
90
-
91
85
  log_info(`handleAction: refreshComponent for dataSource:${dataSource}`, {
92
86
  source,
93
87
  inflatedUrl: dataSource,
@@ -3,8 +3,61 @@ import { ActionResult } from "../ActionExecutor";
3
3
  import { ActionHandler } from "../types";
4
4
  import { postEvent } from "../../reactHooks/useSubscriberFor";
5
5
 
6
+ export const CONFIRMATION_TOAST_SOURCE = "confirmation";
7
+
8
+ export const CONFIRMATION_TOAST_DEFAULT_TIMEOUT = 1000;
9
+
6
10
  /**
7
- * Options for showToast action.
11
+ * Shows a mobile confirmation toast with Theme defaults from
12
+ * `Confirmation Toast / Message` in the base theme plugin.
13
+ *
14
+ * This action is confirmation-only by design: it always stamps
15
+ * `source: "confirmation"` (so Theme confirmation styles apply) and defaults
16
+ * `timeout` to 1000 ms. Non-confirmation toasts (e.g. Offline Experience)
17
+ * must post on the shared `"showToast"` event bus directly and omit `source`.
18
+ * Callers may override individual style fields; Theme supplies the rest.
19
+ *
20
+ * @example From React via ActionExecutorContext
21
+ * ```tsx
22
+ * import React, { useContext } from "react";
23
+ * import { ActionExecutorContext } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutorContext";
24
+ *
25
+ * function QueueButton() {
26
+ * const { handleAction } = useContext(ActionExecutorContext);
27
+ *
28
+ * const onPress = () => {
29
+ * void handleAction({
30
+ * type: "showToast",
31
+ * options: { message: "Added to queue" },
32
+ * });
33
+ * };
34
+ *
35
+ * return <Button onPress={onPress} title="Add to queue" />;
36
+ * }
37
+ * ```
38
+ *
39
+ * @example From code via the actionExecutor singleton
40
+ * ```ts
41
+ * import { actionExecutor } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor";
42
+ *
43
+ * await actionExecutor.handleAction({
44
+ * type: "showToast",
45
+ * options: {
46
+ * message: "Playlist created",
47
+ * style: { backgroundColor: "#00AA55" }, // optional partial override
48
+ * },
49
+ * });
50
+ * ```
51
+ *
52
+ * @example From entry / pipes actions JSON
53
+ * ```json
54
+ * {
55
+ * "type": "showToast",
56
+ * "options": {
57
+ * "message": "Item removed from playlist"
58
+ * }
59
+ * }
60
+ * ```
8
61
  */
9
62
  export interface ShowToastActionOptions {
10
63
  id?: string;
@@ -17,13 +70,16 @@ export interface ShowToastActionOptions {
17
70
  export const showToastAction: ActionHandler<ShowToastActionOptions> = async (
18
71
  action
19
72
  ): Promise<ActionResult> => {
73
+ const options = action?.options;
74
+
20
75
  postEvent("showToast", [
21
76
  {
22
- id: action?.options?.id,
23
- message: action?.options?.message,
24
- extraMessage: action?.options?.extraMessage,
25
- style: action?.options?.style,
26
- timeout: action?.options?.timeout,
77
+ id: options?.id,
78
+ message: options?.message,
79
+ extraMessage: options?.extraMessage,
80
+ style: options?.style,
81
+ timeout: options?.timeout ?? CONFIRMATION_TOAST_DEFAULT_TIMEOUT,
82
+ source: CONFIRMATION_TOAST_SOURCE,
27
83
  },
28
84
  ]);
29
85
 
@@ -27,7 +27,7 @@ function makeMultiSelect(feed: ZappFeed, key, decoratedFeed) {
27
27
  );
28
28
 
29
29
  const behavior = {
30
- ...feed.extensions?.behavior,
30
+ ...feed.extensions?.["behavior"],
31
31
  select_mode: "multi",
32
32
  current_selection: `@{${scope}/${key}}`,
33
33
  };
@@ -75,7 +75,7 @@ function makeSingleSelect(feed: ZappFeed, key, decoratedFeed) {
75
75
  );
76
76
 
77
77
  const behavior = {
78
- ...feed.extensions?.behavior,
78
+ ...feed.extensions?.["behavior"],
79
79
  select_mode: "single",
80
80
  current_selection: `@{${scope}/${key}}`,
81
81
  };
@@ -141,11 +141,11 @@ function makeSingleSelect(feed: ZappFeed, key, decoratedFeed) {
141
141
  }
142
142
 
143
143
  export const decorateFeed = (feed: ZappFeed) => {
144
- if (!(feed.extensions?.role === "preference_editor")) {
144
+ if (!(feed.extensions?.["role"] === "preference_editor")) {
145
145
  return feed;
146
146
  }
147
147
 
148
- const key = feed.extensions?.preference_editor_options?.key;
148
+ const key = feed.extensions?.["preference_editor_options"]?.["key"];
149
149
 
150
150
  if (!key) {
151
151
  log_error(
@@ -160,8 +160,8 @@ export const decorateFeed = (feed: ZappFeed) => {
160
160
  const decoratedFeed = R.clone(feed);
161
161
 
162
162
  const isSingleSelect =
163
- (feed.extensions?.preference_editor_options?.select_mode ||
164
- feed.extensions?.behavior?.select_mode) === "single";
163
+ (feed.extensions?.["preference_editor_options"]?.select_mode ||
164
+ feed.extensions?.["behavior"]?.select_mode) === "single";
165
165
 
166
166
  if (isSingleSelect) {
167
167
  return makeSingleSelect(feed, key, decoratedFeed);
@@ -16,10 +16,8 @@ export interface ActionExecutionContext {
16
16
  /** The entry that defines the screen (if screen was opened from an entry) */
17
17
  screenEntry?: ZappEntry;
18
18
 
19
- /** The UI component that triggered the action.
20
- * From UIComponentContext as `{ ...component, parent }`.
21
- */
22
- component?: ZappUIComponent & { parent?: ZappUIComponent };
19
+ /** The UI component that triggered the action */
20
+ component?: ZappUIComponent;
23
21
 
24
22
  /** The route identifier for the current screen */
25
23
  screenRoute?: string;
@@ -33,7 +31,7 @@ export interface ActionExecutionContext {
33
31
  /** Callback function for action completion */
34
32
  callback?: (result: { success: boolean; error: any; abort: boolean }) => void;
35
33
 
36
- screenState?: Record<any, any>;
34
+ screenState: Record<any, any>;
37
35
  }
38
36
 
39
37
  /**
@@ -0,0 +1,41 @@
1
+ import { retrieveFeedUrl, retrievePlayPreviousFeedUrl } from "../utils";
2
+
3
+ const entryWith = (extensions: Record<string, unknown>): ZappEntry =>
4
+ ({ id: "1", type: { value: "video" }, extensions }) as ZappEntry;
5
+
6
+ describe("playlist feed url accessors", () => {
7
+ it("returns the next feed url when present", () => {
8
+ const entry = entryWith({ play_next_feed_url: "https://feeds/next.json" });
9
+
10
+ expect(retrieveFeedUrl(entry)).toBe("https://feeds/next.json");
11
+ });
12
+
13
+ it("returns the previous feed url when present", () => {
14
+ const entry = entryWith({
15
+ play_previous_feed_url: "https://feeds/prev.json",
16
+ });
17
+
18
+ expect(retrievePlayPreviousFeedUrl(entry)).toBe("https://feeds/prev.json");
19
+ });
20
+
21
+ it("returns undefined when only the next url is present", () => {
22
+ const entry = entryWith({ play_next_feed_url: "https://feeds/next.json" });
23
+
24
+ expect(retrievePlayPreviousFeedUrl(entry)).toBeUndefined();
25
+ });
26
+
27
+ it("returns undefined for an entry with no extensions", () => {
28
+ expect(
29
+ retrievePlayPreviousFeedUrl({
30
+ id: "1",
31
+ type: { value: "video" },
32
+ } as ZappEntry)
33
+ ).toBeUndefined();
34
+ });
35
+
36
+ it("returns undefined for a missing entry", () => {
37
+ expect(
38
+ retrievePlayPreviousFeedUrl(undefined as unknown as ZappEntry)
39
+ ).toBeUndefined();
40
+ });
41
+ });
@@ -43,6 +43,9 @@ export const parseTimeToSeconds = (timeStr: string): number => {
43
43
  export const retrieveFeedUrl = (entry: ZappEntry) =>
44
44
  entry?.extensions?.play_next_feed_url;
45
45
 
46
+ export const retrievePlayPreviousFeedUrl = (entry: ZappEntry) =>
47
+ entry?.extensions?.play_previous_feed_url;
48
+
46
49
  export const retrieveOverlayDuration = (plugin) =>
47
50
  toNumberWithDefault(plugin?.configuration?.overlay_duration, 15);
48
51
 
@@ -0,0 +1,158 @@
1
+ const { player } = require("../defaultManifestConfigurations/player");
2
+
3
+ const manifestFor = (platform) => player({ platform, version: "0.0.0" });
4
+
5
+ const findGroup = (manifest, label) =>
6
+ manifest.styles.fields.find((group) => group.group && group.label === label);
7
+
8
+ const findFieldInGroup = (manifest, label, key) =>
9
+ findGroup(manifest, label)?.fields?.find((field) => field.key === key);
10
+
11
+ const findLocalization = (manifest, key) =>
12
+ manifest.localizations.fields.find((field) => field.key === key);
13
+
14
+ describe("player manifest — audio playlist controls", () => {
15
+ const manifest = manifestFor("ios_for_quickbrick");
16
+
17
+ describe("Player Controls Layout", () => {
18
+ const field = () =>
19
+ findFieldInGroup(manifest, "Audio Player", "player_controls_layout");
20
+
21
+ it("is declared in the Audio Player group", () => {
22
+ expect(field()).toBeDefined();
23
+ });
24
+
25
+ it("is a select defaulting to the existing controls", () => {
26
+ expect(field().type).toBe("select");
27
+ expect(field().initial_value).toBe("playback_speed_and_sleep_timer");
28
+ });
29
+
30
+ it("only shows when the audio player layout is in use", () => {
31
+ expect(field()).toMatchObject({
32
+ rules: "conditional",
33
+ conditional_fields: [
34
+ { key: "styles/full_screen_audio_player", condition_value: true },
35
+ ],
36
+ });
37
+ });
38
+
39
+ it("offers exactly the two documented options", () => {
40
+ expect(field().options).toEqual([
41
+ {
42
+ text: "Playback Speed & Sleep Timer (Default)",
43
+ value: "playback_speed_and_sleep_timer",
44
+ },
45
+ {
46
+ text: "Playlist Controls (Previous & Next)",
47
+ value: "playlist_controls",
48
+ },
49
+ ]);
50
+ });
51
+ });
52
+
53
+ describe("control icons", () => {
54
+ it.each(["previous_track", "next_track"])(
55
+ "declares %s as an uploader",
56
+ (key) => {
57
+ expect(findFieldInGroup(manifest, "Assets", key)).toMatchObject({
58
+ key,
59
+ type: "uploader",
60
+ });
61
+ }
62
+ );
63
+
64
+ it.each(["previous_track", "next_track"])(
65
+ "only shows %s when the playlist controls layout is selected",
66
+ (key) => {
67
+ expect(findFieldInGroup(manifest, "Assets", key)).toMatchObject({
68
+ rules: "conditional",
69
+ conditional_fields: [
70
+ {
71
+ key: "styles/player_controls_layout",
72
+ condition_value: "playlist_controls",
73
+ },
74
+ ],
75
+ });
76
+ }
77
+ );
78
+
79
+ it.each([
80
+ "speed_0_8",
81
+ "speed_1",
82
+ "speed_1_2",
83
+ "speed_1_5",
84
+ "speed_2",
85
+ "sleep_timer",
86
+ "sleep_timer_active",
87
+ ])(
88
+ "only shows %s when the playback speed and sleep timer layout is selected",
89
+ (key) => {
90
+ expect(findFieldInGroup(manifest, "Assets", key)).toMatchObject({
91
+ rules: "conditional",
92
+ conditional_fields: [
93
+ {
94
+ key: "styles/player_controls_layout",
95
+ condition_value: "playback_speed_and_sleep_timer",
96
+ },
97
+ ],
98
+ });
99
+ }
100
+ );
101
+ });
102
+
103
+ describe("accessibility strings", () => {
104
+ it.each([
105
+ "accessibility_previous_track_label",
106
+ "accessibility_previous_track_hint",
107
+ "accessibility_next_track_label",
108
+ "accessibility_next_track_hint",
109
+ ])("declares %s with a default", (key) => {
110
+ expect(findLocalization(manifest, key)).toMatchObject({
111
+ type: "text_input",
112
+ });
113
+
114
+ expect(findLocalization(manifest, key).initial_value).toBeTruthy();
115
+ });
116
+ });
117
+
118
+ describe("tv platforms", () => {
119
+ // The audio player layout is mobile only, so nothing this feature adds —
120
+ // including its screen reader strings — belongs in a tv manifest.
121
+ it.each(["tvos_for_quickbrick", "android_tv_for_quickbrick", "lg_tv"])(
122
+ "adds no audio controls layout field on %s",
123
+ (platform) => {
124
+ expect(
125
+ findFieldInGroup(
126
+ manifestFor(platform),
127
+ "Audio Player",
128
+ "player_controls_layout"
129
+ )
130
+ ).toBeUndefined();
131
+ }
132
+ );
133
+
134
+ it.each([
135
+ "accessibility_previous_track_label",
136
+ "accessibility_previous_track_hint",
137
+ "accessibility_next_track_label",
138
+ "accessibility_next_track_hint",
139
+ ])("does not declare %s on tv", (key) => {
140
+ expect(
141
+ findLocalization(manifestFor("tvos_for_quickbrick"), key)
142
+ ).toBeUndefined();
143
+ });
144
+
145
+ it("still declares the tv-only localizations", () => {
146
+ expect(
147
+ findLocalization(
148
+ manifestFor("tvos_for_quickbrick"),
149
+ "back_to_live_label"
150
+ )
151
+ ).toBeDefined();
152
+
153
+ expect(
154
+ findLocalization(manifestFor("tvos_for_quickbrick"), "start_over_label")
155
+ ).toBeDefined();
156
+ });
157
+ });
158
+ });