@applicaster/zapp-react-native-utils 16.0.0-alpha.1701179921 → 16.0.0-alpha.1733304298

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 (74) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/__tests__/StorageActions.test.ts +98 -0
  3. package/actionsExecutor/__tests__/feedDecorator.test.ts +30 -0
  4. package/actionsExecutor/__tests__/screenToggleFlag.test.ts +90 -0
  5. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +139 -5
  6. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
  7. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +72 -0
  8. package/actionsExecutor/actions/navigateToScreen.ts +29 -45
  9. package/actionsExecutor/actions/openBottomSheet.ts +2 -0
  10. package/actionsExecutor/actions/screenToggleFlag.ts +1 -3
  11. package/actionsExecutor/feedDecorator.ts +6 -0
  12. package/analyticsUtils/__tests__/playerAnalyticsSafeguard.test.ts +62 -0
  13. package/analyticsUtils/playerAnalyticsTracker.ts +1 -0
  14. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -22
  15. package/appUtils/contextKeysManager/__tests__/getKey/success/fullSearch.test.ts +1 -25
  16. package/appUtils/contextKeysManager/__tests__/getKey/success/searchInSecure.test.ts +1 -7
  17. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +0 -25
  18. package/appUtils/contextKeysManager/__tests__/removeKey/success.test.ts +0 -6
  19. package/appUtils/contextKeysManager/__tests__/setKey/failure/failedToSaveInStorage.test.ts +0 -40
  20. package/appUtils/contextKeysManager/__tests__/setKey/{success/saveItemAndReference.test.ts → success.test.ts} +2 -42
  21. package/appUtils/contextKeysManager/index.ts +28 -129
  22. package/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
  23. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +281 -0
  24. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  25. package/appUtils/playerManager/index.ts +7 -0
  26. package/appUtils/playerManager/player.ts +15 -0
  27. package/appUtils/playerManager/playerToolbox.ts +196 -0
  28. package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
  29. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  30. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  31. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  32. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  33. package/cellUtils/index.ts +40 -0
  34. package/configurationUtils/__tests__/modalBlocksParser.test.ts +134 -0
  35. package/configurationUtils/modalBlocksParser.ts +38 -0
  36. package/focusManager/FocusManager.ts +26 -0
  37. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  38. package/localizationUtils/index.ts +12 -6
  39. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  40. package/logger/xrayToolbox.ts +67 -0
  41. package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
  42. package/manifestUtils/keys.js +7 -2
  43. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  44. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  45. package/manifestUtils/mobileAction/groups/defaults.js +2 -1
  46. package/modalState/ModalOrchestrator.ts +1 -0
  47. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +23 -0
  48. package/modalState/index.ts +2 -0
  49. package/modalState/types.ts +11 -0
  50. package/navigationUtils/xrayNavigator.ts +12 -0
  51. package/package.json +3 -2
  52. package/quickBrickAssets/__tests__/index.test.ts +18 -0
  53. package/quickBrickAssets/index.ts +35 -0
  54. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  55. package/reactHooks/actions/index.ts +29 -23
  56. package/reactHooks/connection/index.tsx +8 -5
  57. package/reactHooks/feed/__tests__/feedInvalidation.test.tsx +338 -0
  58. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
  59. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  60. package/reactHooks/feed/useFeedLoader.tsx +28 -1
  61. package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
  62. package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
  63. package/reactHooks/usePluginConfiguration.ts +84 -46
  64. package/riverComponetsMeasurementProvider/index.tsx +3 -1
  65. package/uiActionsRegistrator/__tests__/subscribe.test.ts +26 -0
  66. package/uiActionsRegistrator/registry.ts +16 -0
  67. package/appUtils/contextKeysManager/__tests__/getKey/success/byReference.test.ts +0 -56
  68. package/appUtils/contextKeysManager/__tests__/getKeyByReference/failure.test.ts +0 -265
  69. package/appUtils/contextKeysManager/__tests__/getKeyByReference/success.test.ts +0 -303
  70. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/failure.test.ts +0 -82
  71. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/success.test.ts +0 -41
  72. package/appUtils/contextKeysManager/__tests__/setKey/success/saveItemButNotReference.test.ts +0 -208
  73. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/failure.test.ts +0 -93
  74. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/success.test.ts +0 -45
@@ -28,17 +28,23 @@ const RTL_LOCALES = [
28
28
  "yi", // Yiddish
29
29
  ];
30
30
 
31
+ // The strings for the UI language, else the first language present
32
+ export const getLocalizedStrings = (
33
+ localizations = {},
34
+ languageCode = getUILanguage()
35
+ ) =>
36
+ localizations?.[languageCode] ||
37
+ Object.values(localizations).find(
38
+ (value) => typeof value === "object" && value !== null
39
+ ) ||
40
+ {};
41
+
31
42
  export function useLocalizedStrings({ localizations = {} }) {
32
43
  const languageCode = getUILanguage();
33
44
 
34
45
  // TODO: probably we can call getLocalizations here from localizationsHelper.js
35
46
  const result = useMemo(
36
- () =>
37
- localizations?.[languageCode] ||
38
- Object.values(localizations).find(
39
- (value) => typeof value === "object" && value !== null
40
- ) ||
41
- {},
47
+ () => getLocalizedStrings(localizations, languageCode),
42
48
  [languageCode, localizations]
43
49
  );
44
50
 
@@ -0,0 +1,69 @@
1
+ const xray = require("@applicaster/quick-brick-xray");
2
+
3
+ const toolbox = { name: "test", description: "", tools: [] };
4
+
5
+ const load = (): typeof import("../xrayToolbox") => {
6
+ let module;
7
+
8
+ jest.isolateModules(() => {
9
+ module = require("../xrayToolbox");
10
+ });
11
+
12
+ return module;
13
+ };
14
+
15
+ describe("registerXrayToolbox", () => {
16
+ afterEach(() => {
17
+ delete xray.registerReactToolbox;
18
+ });
19
+
20
+ it("registers once per name", async () => {
21
+ xray.registerReactToolbox = jest.fn().mockResolvedValue(undefined);
22
+ const { registerXrayToolbox } = load();
23
+
24
+ await expect(registerXrayToolbox(toolbox)).resolves.toBe(true);
25
+ await expect(registerXrayToolbox(toolbox)).resolves.toBe(true);
26
+
27
+ expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
28
+ });
29
+
30
+ it("resolves false without a toolbox API", async () => {
31
+ await expect(load().registerXrayToolbox(toolbox)).resolves.toBe(false);
32
+ });
33
+
34
+ it("keeps a failed registration instead of retrying it", async () => {
35
+ xray.registerReactToolbox = jest
36
+ .fn()
37
+ .mockRejectedValue(new Error("XRayToolboxBridge is not available"));
38
+
39
+ const { registerXrayToolbox } = load();
40
+
41
+ await expect(registerXrayToolbox(toolbox)).resolves.toBe(false);
42
+ await expect(registerXrayToolbox(toolbox)).resolves.toBe(false);
43
+
44
+ expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
45
+ });
46
+
47
+ it("registers new tools under the same name again (Fast Refresh)", async () => {
48
+ xray.registerReactToolbox = jest.fn().mockResolvedValue(undefined);
49
+ const { registerXrayToolbox } = load();
50
+
51
+ await registerXrayToolbox(toolbox);
52
+ await registerXrayToolbox({ ...toolbox, tools: [] });
53
+
54
+ expect(xray.registerReactToolbox).toHaveBeenCalledTimes(2);
55
+ });
56
+
57
+ it("stops trying every toolbox once the bridge is missing", async () => {
58
+ xray.registerReactToolbox = jest.fn().mockRejectedValue(new Error("no"));
59
+ const { registerXrayToolbox } = load();
60
+
61
+ await registerXrayToolbox(toolbox);
62
+
63
+ await expect(
64
+ registerXrayToolbox({ ...toolbox, name: "other" })
65
+ ).resolves.toBe(false);
66
+
67
+ expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
68
+ });
69
+ });
@@ -0,0 +1,67 @@
1
+ import { getXray } from ".";
2
+
3
+ /** One tool of an X-Ray React toolbox: an agent calls it by name with JSON arguments. */
4
+ export type XrayToolSpec = {
5
+ name: string;
6
+ description: string;
7
+ parameters?: Array<{
8
+ name: string;
9
+ type: "number" | "string";
10
+ description: string;
11
+ }>;
12
+ handler: (args: Record<string, unknown>) => unknown;
13
+ };
14
+
15
+ export type XrayToolbox = {
16
+ name: string;
17
+ description: string;
18
+ tools: XrayToolSpec[];
19
+ };
20
+
21
+ type RegisterReactToolbox = (toolbox: XrayToolbox) => Promise<void>;
22
+
23
+ const registered = new Map<
24
+ string,
25
+ { tools: XrayToolSpec[]; registration: Promise<boolean> }
26
+ >();
27
+
28
+ /** Set once a registration fails: the build has no X-Ray bridge, which cannot appear later. */
29
+ let bridgeMissing = false;
30
+
31
+ /**
32
+ * Registers an X-Ray React toolbox and resolves whether it is registered. The same tools are
33
+ * registered once; new tools under the same name (Fast Refresh re-ran the module) replace them.
34
+ * A build without the X-Ray native bridge (release builds) rejects the first attempt and every
35
+ * later call resolves false at once, so callers on hot paths pay for one attempt only.
36
+ */
37
+ export const registerXrayToolbox = (toolbox: XrayToolbox): Promise<boolean> => {
38
+ const cached = registered.get(toolbox.name);
39
+
40
+ if (cached?.tools === toolbox.tools) return cached.registration;
41
+
42
+ if (bridgeMissing) return Promise.resolve(false);
43
+
44
+ const register = (
45
+ getXray() as
46
+ | { registerReactToolbox?: RegisterReactToolbox }
47
+ | null
48
+ | undefined
49
+ )?.registerReactToolbox;
50
+
51
+ const registration = register
52
+ ? register(toolbox).then(
53
+ () => true,
54
+ () => {
55
+ bridgeMissing = true;
56
+
57
+ return false;
58
+ }
59
+ )
60
+ : Promise.resolve(false);
61
+
62
+ if (!register) bridgeMissing = true;
63
+
64
+ registered.set(toolbox.name, { tools: toolbox.tools, registration });
65
+
66
+ return registration;
67
+ };
@@ -830,6 +830,14 @@ function getPlayerConfiguration({ platform, version }) {
830
830
  type: "switch",
831
831
  label_tooltip:
832
832
  "Enable this option to have a floating video player when the app is in the background.",
833
+ },
834
+ {
835
+ key: "vertical_video_player",
836
+ label: "Vertical video mode",
837
+ initial_value: false,
838
+ type: "switch",
839
+ label_tooltip:
840
+ "Enable this option to use the vertical video player layout.",
833
841
  }
834
842
  );
835
843
 
@@ -412,6 +412,12 @@ const mobileActionButtonContainerFields = (positionOptions) => [
412
412
  type: ZAPPIFEST_FIELDS.switch,
413
413
  suffix: "buttons enabled",
414
414
  },
415
+ {
416
+ type: ZAPPIFEST_FIELDS.switch,
417
+ suffix: "cell navigation enabled",
418
+ label_tooltip:
419
+ "Controls whether tapping on the cell (outside the buttons) triggers its navigation behavior. When disabled, only the configured buttons are interactive. Useful for Home screens (tap anywhere to open) vs. Details screens (only buttons should work).",
420
+ },
415
421
  {
416
422
  type: ZAPPIFEST_FIELDS.select,
417
423
  suffix: "position",
@@ -472,14 +478,13 @@ const MOBILE_ACTION_BUTTON_FIELDS = [
472
478
  suffix: "assign action",
473
479
  options: [
474
480
  { text: "primary navigation", value: "navigation_action" },
475
- { text: "secondary navigation", value: "secondary_navigation" },
476
481
  { text: "favorite", value: "local_storage_favourites_action" },
477
482
  { text: "more", value: "open-modal-bottom-sheet-cell-action" },
478
483
  { text: "add to calendar", value: "add_to_calendar_action" },
479
484
  { text: "share", value: "native_share_action" },
480
485
  { text: "downloads", value: "offline-content-button" },
481
486
  { text: "trailer", value: "trailer_action" },
482
- { text: "mute/unmute", value: "mute_unmute" },
487
+ { text: "mute/unmute", value: "mute_unmute_action" },
483
488
  ],
484
489
  },
485
490
  {
@@ -7,6 +7,7 @@ describe("mobileActionButtonsContainer", () => {
7
7
  description: "container",
8
8
  defaults: {
9
9
  buttonsEnabled: false,
10
+ cellNavigationEnabled: false,
10
11
  position: ["over_image", "text_label_1", "text_label_2"],
11
12
  align: "left",
12
13
  overImagePosition: "bottom_left",
@@ -31,6 +32,26 @@ describe("mobileActionButtonsContainer", () => {
31
32
  expect(enabledField).toBeTruthy();
32
33
  expect(enabledField.type).toBe("switch");
33
34
 
35
+ const cellNavigationField = result.fields.find(
36
+ (field) =>
37
+ field.key === "mobile_buttons_container_cell_navigation_enabled"
38
+ );
39
+
40
+ expect(cellNavigationField).toMatchObject({
41
+ type: "switch",
42
+ label: "Cell navigation enabled",
43
+ initial_value: false,
44
+ label_tooltip: expect.stringContaining(
45
+ "Controls whether tapping on the cell"
46
+ ),
47
+ conditional_fields: [
48
+ {
49
+ key: "styles/mobile_buttons_container_buttons_enabled",
50
+ condition_value: true,
51
+ },
52
+ ],
53
+ });
54
+
34
55
  const positionField = result.fields.find(
35
56
  (field) => field.key === "mobile_buttons_container_position"
36
57
  );
@@ -67,7 +67,7 @@ describe("buildMobileActionButtonGroups", () => {
67
67
  );
68
68
 
69
69
  expect(button2EnabledField.initial_value).toBe(false);
70
- expect(button2AssignActionField.initial_value).toBe("secondary_navigation");
70
+ expect(button2AssignActionField.initial_value).toBe("trailer_action");
71
71
 
72
72
  const button3Fields = groups[3].fields;
73
73
 
@@ -1,5 +1,6 @@
1
1
  const DEFAULT_MOBILE_ACTION_BUTTONS_CONTAINER_DEFAULTS = {
2
2
  buttonsEnabled: false,
3
+ cellNavigationEnabled: false,
3
4
 
4
5
  align: "left",
5
6
  overImagePosition: "bottom_left",
@@ -63,7 +64,7 @@ const DEFAULT_MOBILE_ACTION_BUTTON_PRESETS = {
63
64
  },
64
65
  2: {
65
66
  buttonEnabled: false,
66
- assignAction: "secondary_navigation",
67
+ assignAction: "trailer_action",
67
68
  },
68
69
  3: {
69
70
  buttonEnabled: false,
@@ -45,6 +45,7 @@ function menuToModalArgs(
45
45
  summary: menu.header?.subtitle,
46
46
  styleOverrides: menu.content.styleOverrides,
47
47
  emptyMessage: menu.content.emptyMessage,
48
+ operationButtons: menu.content.operationButtons,
48
49
  onPress: onItemPress,
49
50
  viewModel,
50
51
  },
@@ -60,4 +60,27 @@ describe("ModalOrchestrator phase 3", () => {
60
60
 
61
61
  expect(props.emptyMessage).toBe("Queued items will be added here");
62
62
  });
63
+
64
+ it("forwards operationButtons onto the sheet props unchanged", () => {
65
+ const operationButtons = {
66
+ add: {
67
+ title: "Create New Playlist",
68
+ iconURL: "https://example.com/add.png",
69
+ },
70
+ };
71
+
72
+ modalOrchestrator.open({
73
+ header: { title: "Playlists" },
74
+ content: {
75
+ title: "Playlists",
76
+ items: [],
77
+ operationButtons,
78
+ },
79
+ });
80
+
81
+ const props = (modalStore.getState().modalState.screen as any)
82
+ ?.modalBottomSheetContentProps;
83
+
84
+ expect(props.operationButtons).toEqual(operationButtons);
85
+ });
63
86
  });
@@ -23,6 +23,8 @@ export type {
23
23
  SecondaryAction,
24
24
  SelectMode,
25
25
  SelectionBehavior,
26
+ OperationButton,
27
+ OperationButtons,
26
28
  } from "./types";
27
29
 
28
30
  export {
@@ -1,3 +1,5 @@
1
+ import type { EntryAction } from "../entryActions/types";
2
+
1
3
  export interface Header {
2
4
  title: string;
3
5
  subtitle?: string;
@@ -107,6 +109,13 @@ export interface DynamicCollectionOptions {
107
109
  events?: Record<CollectionScopedOperation | string, Action[]>;
108
110
  }
109
111
 
112
+ /** Presentation of a sheet-synthesized operation control; same fields as an entry action button. */
113
+ export type OperationButton = Pick<EntryAction["button"], "title" | "iconURL">;
114
+
115
+ export type OperationButtons = Partial<
116
+ Record<CollectionScopedOperation | string, OperationButton>
117
+ >;
118
+
110
119
  export interface Content {
111
120
  title: string;
112
121
  stickyTitle?: boolean;
@@ -120,6 +129,8 @@ export interface Content {
120
129
  styleOverrides?: Record<string, any>;
121
130
  /** Shown under Next Up when the queue sheet has no upcoming items. */
122
131
  emptyMessage?: string;
132
+ /** Producer-owned presentation for sheet-synthesized operation controls (e.g. `add`). */
133
+ operationButtons?: OperationButtons;
123
134
  context?: Record<string, any>;
124
135
  }
125
136
 
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The app navigator for the X-Ray toolboxes (nav, screen), which run outside React. The
3
+ * NavigationProvider sets it after each commit; it is a plain module variable, so updating it
4
+ * never renders anything.
5
+ */
6
+ let navigator: QuickBrickAppNavigator | undefined;
7
+
8
+ export const setXrayNavigator = (value: QuickBrickAppNavigator | undefined) => {
9
+ navigator = value;
10
+ };
11
+
12
+ export const getXrayNavigator = () => navigator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "16.0.0-alpha.1701179921",
3
+ "version": "16.0.0-alpha.1733304298",
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": "16.0.0-alpha.1701179921",
30
+ "@applicaster/applicaster-types": "16.0.0-alpha.1733304298",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -36,6 +36,7 @@
36
36
  "prop-types": "^15.0.0"
37
37
  },
38
38
  "peerDependencies": {
39
+ "@applicaster/quick-brick-assets": "*",
39
40
  "@applicaster/zapp-pipes-v2-client": "*",
40
41
  "@react-native-community/netinfo": "*",
41
42
  "react": "*",
@@ -0,0 +1,18 @@
1
+ // The Jest stand-in for @applicaster/quick-brick-assets maps each existing key to itself
2
+ import { getQuickBrickAsset } from "..";
3
+
4
+ describe("getQuickBrickAsset", () => {
5
+ it("returns the asset for a plugin identifier and path", () => {
6
+ expect(
7
+ getQuickBrickAsset("mute_unmute_action", "action/flavour_1/mute.png")
8
+ ).toBe("mute_unmute_action/action/flavour_1/mute.png");
9
+ });
10
+
11
+ it("rejects unknown paths and plugin identifiers at compile time", () => {
12
+ // @ts-expect-error - no such file in mute_unmute_action
13
+ getQuickBrickAsset("mute_unmute_action", "action/flavour_9/mute.png");
14
+
15
+ // @ts-expect-error - no assets folder for this plugin
16
+ getQuickBrickAsset("unknown_plugin", "action/flavour_1/mute.png");
17
+ });
18
+ });
@@ -0,0 +1,35 @@
1
+ import assets from "@applicaster/quick-brick-assets";
2
+ import type {
3
+ QuickBrickAssetKey,
4
+ QuickBrickAssetSource,
5
+ } from "@applicaster/quick-brick-assets";
6
+
7
+ export type { QuickBrickAssetKey, QuickBrickAssetSource };
8
+
9
+ type PluginOf<K> = K extends `${infer Plugin}/${string}` ? Plugin : never;
10
+
11
+ type PathOf<K, Plugin extends string> = K extends `${Plugin}/${infer Path}`
12
+ ? Path
13
+ : never;
14
+
15
+ /** Plugin identifiers that have a folder in `@applicaster/quick-brick-assets`. */
16
+ export type QuickBrickAssetPlugin = PluginOf<QuickBrickAssetKey>;
17
+
18
+ /** Asset paths inside the given plugin's folder. */
19
+ export type QuickBrickAssetPath<Plugin extends QuickBrickAssetPlugin> = PathOf<
20
+ QuickBrickAssetKey,
21
+ Plugin
22
+ >;
23
+
24
+ /**
25
+ * Looks up an asset of `@applicaster/quick-brick-assets` by plugin identifier
26
+ * and path relative to the plugin folder, e.g.
27
+ * `getQuickBrickAsset("mute_unmute_action", "action/flavour_1/mute.png")`.
28
+ * Both arguments are checked against the package's files at compile time.
29
+ * Density variants (`@2x` / `@3x`) are not supported.
30
+ */
31
+ export const getQuickBrickAsset = <Plugin extends QuickBrickAssetPlugin>(
32
+ pluginIdentifier: Plugin,
33
+ assetPath: QuickBrickAssetPath<Plugin>
34
+ ): QuickBrickAssetSource =>
35
+ assets[`${pluginIdentifier}/${assetPath}` as QuickBrickAssetKey];
@@ -0,0 +1,82 @@
1
+ import { renderHook } from "@testing-library/react-native";
2
+
3
+ const plugin = {
4
+ identifier: "cast",
5
+ configuration: {
6
+ title: "Configured",
7
+ localizations: { fr: { close: "Fermer" } },
8
+ },
9
+ };
10
+
11
+ jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
12
+ usePlugins: () => [plugin],
13
+ }));
14
+
15
+ jest.mock(
16
+ "@applicaster/zapp-react-native-utils/appUtils/localizationsHelper",
17
+ () => ({ getUILanguage: () => "fr" })
18
+ );
19
+
20
+ import {
21
+ getPluginConfiguration,
22
+ mergeLocalizations,
23
+ usePluginConfiguration,
24
+ } from "../usePluginConfiguration";
25
+
26
+ const manifest = {
27
+ custom_configuration_fields: [
28
+ { key: "title", initial_value: "Default" },
29
+ { key: "color", initial_value: "red" },
30
+ ],
31
+ localizations: {
32
+ fields: [
33
+ { key: "close", initial_value: "Close" },
34
+ { key: "cancel", initial_value: "Cancel" },
35
+ ],
36
+ },
37
+ };
38
+
39
+ it("matches usePluginConfiguration outside React", () => {
40
+ const { result } = renderHook(() => usePluginConfiguration("cast", manifest));
41
+
42
+ expect(getPluginConfiguration(plugin, manifest)).toEqual(result.current);
43
+ });
44
+
45
+ // Expected values are what R.mergeDeepWith((l, r) => (isNil(l) ? r : l)) returned before ramda was removed
46
+ describe("mergeLocalizations", () => {
47
+ const defaults = { en: { close: "Close", cancel: "Cancel" } };
48
+
49
+ it("returns the defaults when nothing is configured", () => {
50
+ expect(mergeLocalizations(undefined, defaults)).toEqual(defaults);
51
+ });
52
+
53
+ it("keeps configured values, fills nil ones and missing keys from defaults", () => {
54
+ expect(
55
+ mergeLocalizations(
56
+ { en: { close: null, extra: "X" }, fr: { close: "Fermer" } },
57
+ defaults
58
+ )
59
+ ).toEqual({
60
+ en: { close: "Close", extra: "X", cancel: "Cancel" },
61
+ fr: { close: "Fermer" },
62
+ });
63
+ });
64
+
65
+ it("does not merge arrays", () => {
66
+ expect(
67
+ mergeLocalizations(
68
+ { en: { close: ["a"] } },
69
+ { en: { close: ["b"], cancel: "C" } }
70
+ )
71
+ ).toEqual({ en: { close: ["a"], cancel: "C" } });
72
+ });
73
+ });
74
+
75
+ it("keeps configured languages first, so the fallback language stays the configured one", () => {
76
+ const merged = mergeLocalizations(
77
+ { he: { close: "סגור" }, en: { close: "Close it" } },
78
+ { en: { close: "Close" } }
79
+ );
80
+
81
+ expect(Object.keys(merged)).toEqual(["he", "en"]);
82
+ });
@@ -21,17 +21,45 @@ type ActionContexts<T = unknown> = {
21
21
  };
22
22
 
23
23
  /**
24
- * React Hook to subscribe to the custom action context
24
+ * A value that changes whenever a provider is added to or removed from
25
+ * `uiActionsRegistry`.
26
+ *
27
+ * Providers can register after the UI that wants them has rendered - the audio
28
+ * player registers playback speed only once its controller has state, and
29
+ * plugin-backed actions are bridged in from an effect. Put this in the
30
+ * dependency list of anything that resolves actions, so the list is re-read
31
+ * instead of being frozen at first paint.
32
+ */
33
+ export function useRegistryRevision(): number {
34
+ const [revision, setRevision] = useState(0);
35
+
36
+ useEffect(
37
+ () =>
38
+ uiActionsRegistry.subscribe(() => setRevision((current) => current + 1)),
39
+ []
40
+ );
41
+
42
+ return revision;
43
+ }
44
+
45
+ /**
46
+ * Resolves a legacy action context or falls back to the UI action registry.
25
47
  * @param {*} plugId - subscribes to the context of that action plugin
26
48
  * @return {*} - global action provider context or context for specific action plugin
27
49
  */
28
50
  export function useActions<T = unknown>(plugId: string): any {
29
51
  const [logged, setLogged] = useState(false);
30
52
  const context = useContext<ActionContexts<T>>(ActionsContext);
53
+ useRegistryRevision();
54
+
31
55
  const actionContext = context?.actions?.[plugId]?.module?.context;
32
56
 
33
57
  if (plugId) {
34
58
  if (!actionContext) {
59
+ const registeredAction = uiActionsRegistry.getAction(plugId);
60
+
61
+ if (registeredAction) return registeredAction;
62
+
35
63
  if (!logged) {
36
64
  logger.warning({
37
65
  message: `useActions: Couldn't find an action for ${plugId} plugin`,
@@ -139,25 +167,3 @@ export function useEntryActionState(
139
167
 
140
168
  return { state, invokeAction };
141
169
  }
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
- }
@@ -4,6 +4,11 @@ import { NetInfoState } from "@react-native-community/netinfo";
4
4
  import { NetworkStatusContext } from "@applicaster/zapp-react-native-ui-components/Contexts/NetworkStatusContext";
5
5
  import { isOnline } from "@applicaster/quick-brick-core/App/NetworkStatusProvider/utils";
6
6
 
7
+ type ConnectionInfo = NetInfoState & {
8
+ deviceStatus: NetStatus;
9
+ stillOfflineAfterResumeCount?: number;
10
+ };
11
+
7
12
  /**
8
13
  * This hook returns connection type from RN NetInfo component or boolean for iConnected if justOnlineStatus flag is set
9
14
  * @param {boolean} justOnlineStatus - flag if to return boolean for isConnected
@@ -12,16 +17,14 @@ import { isOnline } from "@applicaster/quick-brick-core/App/NetworkStatusProvide
12
17
 
13
18
  export function useConnectionInfo(justOnlineStatus: true): boolean;
14
19
 
15
- export function useConnectionInfo(
16
- justOnlineStatus: false
17
- ): NetInfoState & { deviceStatus: NetStatus };
20
+ export function useConnectionInfo(justOnlineStatus: false): ConnectionInfo;
18
21
 
19
22
  export function useConnectionInfo(
20
23
  justOnlineStatus: boolean
21
- ): boolean | (NetInfoState & { deviceStatus: NetStatus }) {
24
+ ): boolean | ConnectionInfo {
22
25
  const connectionInfo = React.useContext(
23
26
  NetworkStatusContext
24
- ) as NetInfoState & { deviceStatus: NetStatus };
27
+ ) as ConnectionInfo;
25
28
 
26
29
  return justOnlineStatus ? isOnline(connectionInfo) === true : connectionInfo;
27
30
  }