@applicaster/zapp-react-native-utils 16.0.0-alpha.2295427024 → 16.0.0-alpha.2480030155

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 (77) hide show
  1. package/actionsExecutor/ActionExecutor.ts +8 -10
  2. package/actionsExecutor/ActionExecutorContext.tsx +40 -345
  3. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  4. package/actionsExecutor/actions/appRestart.ts +24 -0
  5. package/actionsExecutor/actions/confirmDialog.ts +53 -0
  6. package/actionsExecutor/actions/index.ts +30 -0
  7. package/actionsExecutor/actions/localStorageRemove.ts +27 -0
  8. package/actionsExecutor/actions/localStorageSet.ts +28 -0
  9. package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
  10. package/actionsExecutor/actions/navigateToScreen.ts +123 -0
  11. package/actionsExecutor/actions/refreshComponent.ts +79 -0
  12. package/actionsExecutor/actions/screenSetVariable.ts +35 -0
  13. package/actionsExecutor/actions/screenToggleFlag.ts +38 -0
  14. package/actionsExecutor/actions/sendCloudEvent.ts +88 -0
  15. package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
  16. package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
  17. package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
  18. package/actionsExecutor/actions/showToast.ts +87 -0
  19. package/actionsExecutor/actions/switchLayout.ts +40 -0
  20. package/actionsExecutor/types.ts +59 -0
  21. package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
  22. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -1
  23. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -1
  24. package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
  25. package/appUtils/contextKeysManager/index.ts +1 -1
  26. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +12 -6
  27. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  28. package/appUtils/playerManager/{conts.ts → const.ts} +20 -0
  29. package/appUtils/playerManager/index.ts +1 -1
  30. package/appUtils/playerManager/player.ts +1 -1
  31. package/appUtils/playerManager/playerFactory.ts +1 -1
  32. package/appUtils/playerManager/playerNative.ts +1 -1
  33. package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
  34. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  35. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  36. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  37. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  38. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  39. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  40. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  41. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  42. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  43. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  44. package/arrayUtils/__tests__/sample.test.ts +61 -0
  45. package/arrayUtils/index.ts +57 -20
  46. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  47. package/bottomSheet/index.ts +17 -0
  48. package/manifestUtils/_internals/index.js +6 -0
  49. package/manifestUtils/defaultManifestConfigurations/player.js +19 -19
  50. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
  51. package/manifestUtils/fieldUtils/index.js +125 -0
  52. package/manifestUtils/keys.js +8 -0
  53. package/manifestUtils/mobileAction/button/index.js +16 -0
  54. package/manifestUtils/mobileAction/container/index.js +3 -1
  55. package/manifestUtils/mobileAction/groups/defaults.js +3 -1
  56. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  57. package/numberUtils/__tests__/isZero.test.ts +27 -0
  58. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  59. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  60. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  61. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  62. package/numberUtils/index.ts +27 -8
  63. package/package.json +2 -2
  64. package/reactHooks/actions/index.ts +51 -1
  65. package/reactHooks/cell-click/index.ts +3 -3
  66. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  67. package/reactHooks/layout/index.ts +1 -1
  68. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  69. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  70. package/reactHooks/navigation/index.ts +1 -1
  71. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  72. package/reactHooks/state/useComponentScreenState.ts +1 -1
  73. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  74. package/stringUtils/index.ts +2 -2
  75. package/uiActionsRegistrator/index.ts +203 -0
  76. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  77. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
@@ -1,3 +1,121 @@
1
+ const { ZAPPIFEST_FIELDS } = require("../keys");
2
+ const { toCamelCase } = require("../_internals");
3
+
4
+ const SIDES = ["top", "right", "bottom", "left"];
5
+
6
+ /**
7
+ * Parses a CSS margin/padding shorthand string into [top, right, bottom, left].
8
+ *
9
+ * 1 value → all four sides
10
+ * 2 values → top/bottom, right/left
11
+ * 3 values → top, right/left, bottom
12
+ * 4 values → top, right, bottom, left
13
+ *
14
+ * @param {string} str
15
+ * @returns {[number, number, number, number]}
16
+ */
17
+ function parseShorthand(str) {
18
+ const parts = String(str).trim().split(/\s+/).map(Number);
19
+
20
+ switch (parts.length) {
21
+ case 1:
22
+ return [parts[0], parts[0], parts[0], parts[0]];
23
+ case 2:
24
+ return [parts[0], parts[1], parts[0], parts[1]];
25
+ case 3:
26
+ return [parts[0], parts[1], parts[2], parts[1]];
27
+ case 4:
28
+ return parts;
29
+ default:
30
+ throw new Error(
31
+ `Shorthand expects 1–4 space-separated values, got ${parts.length}: "${str}"`
32
+ );
33
+ }
34
+ }
35
+
36
+ function spacingFields(prefix, initialValues) {
37
+ const platforms = Object.keys(initialValues);
38
+
39
+ const parsed = Object.fromEntries(
40
+ platforms.map((p) => [p, parseShorthand(initialValues[p])])
41
+ );
42
+
43
+ return SIDES.map((side, i) => ({
44
+ suffix: `${prefix} ${side}`,
45
+ type: ZAPPIFEST_FIELDS.number_input,
46
+ initialValue: Object.fromEntries(platforms.map((p) => [p, parsed[p][i]])),
47
+ }));
48
+ }
49
+
50
+ /**
51
+ * Generates four margin field descriptors (top/right/bottom/left) from a
52
+ * per-platform CSS shorthand string.
53
+ *
54
+ * @param {string} suffix - prefix word for generated field names, e.g. "margin"
55
+ * @param {Record<string, string>} initialValues - e.g. { mobile: "0 0 20", tv: "0 0 90" }
56
+ * @returns {object[]}
57
+ */
58
+ function margin(suffix, initialValues) {
59
+ return spacingFields(suffix, initialValues);
60
+ }
61
+
62
+ /**
63
+ * Generates four padding field descriptors (top/right/bottom/left) from a
64
+ * per-platform CSS shorthand string.
65
+ *
66
+ * @param {string} suffix - prefix word for generated field names, e.g. "padding"
67
+ * @param {Record<string, string>} initialValues - e.g. { mobile: "0 20", tv: "0 124" }
68
+ * @returns {object[]}
69
+ */
70
+ function padding(suffix, initialValues) {
71
+ return spacingFields(suffix, initialValues);
72
+ }
73
+
74
+ /**
75
+ * Returns a flat camelCase defaults object for a spacing shorthand.
76
+ * e.g. marginDefaults("margin", "0 0 20") → { marginTop: 0, marginRight: 0, marginBottom: 20, marginLeft: 0 }
77
+ *
78
+ * @param {string} suffix - e.g. "margin" or "padding"
79
+ * @param {string} shorthand - CSS-like shorthand string
80
+ * @returns {Record<string, number>}
81
+ */
82
+ function spacingDefaults(suffix, shorthand) {
83
+ const values = parseShorthand(shorthand);
84
+
85
+ return Object.fromEntries(
86
+ SIDES.map((side, i) => [toCamelCase(`${suffix} ${side}`), values[i]])
87
+ );
88
+ }
89
+
90
+ function marginDefaults(suffix, shorthand) {
91
+ return spacingDefaults(suffix, shorthand);
92
+ }
93
+
94
+ function paddingDefaults(suffix, shorthand) {
95
+ return spacingDefaults(suffix, shorthand);
96
+ }
97
+
98
+ /**
99
+ * Returns four field descriptors (top/right/bottom/left) with only suffix and type — no initialValue.
100
+ * Used when defaults are stored separately in the subgroup spec.
101
+ *
102
+ * @param {string} prefix - e.g. "margin" or "padding"
103
+ * @returns {object[]}
104
+ */
105
+ function marginFields(prefix) {
106
+ return SIDES.map((side) => ({
107
+ suffix: `${prefix} ${side}`,
108
+ type: ZAPPIFEST_FIELDS.number_input,
109
+ }));
110
+ }
111
+
112
+ function paddingFields(prefix) {
113
+ return SIDES.map((side) => ({
114
+ suffix: `${prefix} ${side}`,
115
+ type: ZAPPIFEST_FIELDS.number_input,
116
+ }));
117
+ }
118
+
1
119
  /**
2
120
  * Appends new conditional_fields from conditions array
3
121
  * to config.conditional_fields
@@ -48,6 +166,13 @@ function createConditionalField(key, condition_value, category = "styles") {
48
166
  }
49
167
 
50
168
  module.exports = {
169
+ parseShorthand,
170
+ margin,
171
+ padding,
172
+ marginDefaults,
173
+ paddingDefaults,
174
+ marginFields,
175
+ paddingFields,
51
176
  withConditional,
52
177
  getConditionalKey,
53
178
  createConditionalField,
@@ -631,6 +631,14 @@ const MOBILE_ACTION_BUTTON_FIELDS = [
631
631
  type: ZAPPIFEST_FIELDS.number_input,
632
632
  suffix: "margin left",
633
633
  },
634
+ {
635
+ type: ZAPPIFEST_FIELDS.number_input,
636
+ suffix: "horizontal gutter",
637
+ },
638
+ {
639
+ type: ZAPPIFEST_FIELDS.number_input,
640
+ suffix: "vertical gutter",
641
+ },
634
642
  ];
635
643
 
636
644
  const TV_MENU_LABEL_FIELDS = [
@@ -129,6 +129,22 @@ function mobileActionButton({ label, description, defaults, isFirstButton }) {
129
129
  conditions.push(createConditionalField(labelEnabledKey, true));
130
130
  }
131
131
 
132
+ const assetAlignmentKey = keyPrefixGenerator("asset_alignment");
133
+
134
+ // horizontal_gutter fields depends on [asset_alignment: left or asset_alignment: right]
135
+ if (isKeyHasSuffix("horizontal_gutter", key)) {
136
+ conditions.push(
137
+ createConditionalField(assetAlignmentKey, ["left", "right"])
138
+ );
139
+ }
140
+
141
+ // vertical_gutter fields depends on [asset_alignment: above or asset_alignment: below]
142
+ if (isKeyHasSuffix("vertical_gutter", key)) {
143
+ conditions.push(
144
+ createConditionalField(assetAlignmentKey, ["above", "below"])
145
+ );
146
+ }
147
+
132
148
  return withConditional(conditions)(field);
133
149
  });
134
150
 
@@ -49,7 +49,9 @@ function mobileActionButtonsContainer({ label, description, defaults }) {
49
49
 
50
50
  // over_image_position depends on [positionKey: over_image]
51
51
  if (isKeyHasSuffix("over_image_position", key)) {
52
- conditions.push(createConditionalField(positionKey, "over_image"));
52
+ conditions.push(
53
+ createConditionalField(positionKey, defaults.position[0])
54
+ );
53
55
  }
54
56
 
55
57
  // horizontal_gutter depends on [stackingKey: horizontal]
@@ -33,7 +33,7 @@ const DEFAULT_MOBILE_ACTION_BUTTON_SHARED_DEFAULTS = {
33
33
  assetHeight: 24,
34
34
  assetWidth: 24,
35
35
  assetMarginTop: 0,
36
- assetMarginRight: 6,
36
+ assetMarginRight: 0,
37
37
  assetMarginBottom: 0,
38
38
  assetMarginLeft: 0,
39
39
  labelEnabled: true,
@@ -50,6 +50,8 @@ const DEFAULT_MOBILE_ACTION_BUTTON_SHARED_DEFAULTS = {
50
50
  marginRight: 0,
51
51
  marginBottom: 0,
52
52
  marginLeft: 0,
53
+ horizontalGutter: 8,
54
+ verticalGutter: 8,
53
55
  };
54
56
 
55
57
  const DEFAULT_MOBILE_ACTION_BUTTON_PRESETS = {
@@ -0,0 +1,20 @@
1
+ import { isMinusZero } from "..";
2
+
3
+ describe("isMinusZero", () => {
4
+ it("return true for minus zero", () => {
5
+ expect(isMinusZero(-0)).toBe(true);
6
+ });
7
+
8
+ it("return false for positive zero", () => {
9
+ expect(isMinusZero(0)).toBe(false);
10
+ });
11
+
12
+ it("return false for non-zero numbers", () => {
13
+ const inputs = [-1, 1, 100, 0.1, -0.1, Infinity, -Infinity];
14
+ expect.assertions(inputs.length);
15
+
16
+ inputs.forEach((input) => {
17
+ expect(isMinusZero(input)).toBe(false);
18
+ });
19
+ });
20
+ });
@@ -0,0 +1,27 @@
1
+ import { isZero } from "..";
2
+
3
+ describe("isZero", () => {
4
+ it("return true for zero and minus zero", () => {
5
+ expect(isZero(0)).toBe(true);
6
+ expect(isZero(-0)).toBe(true);
7
+ });
8
+
9
+ it("return false for non-zero numbers", () => {
10
+ const inputs = [-1, 1, 100, 0.1, -0.1, Infinity, -Infinity, NaN];
11
+ expect.assertions(inputs.length);
12
+
13
+ inputs.forEach((input) => {
14
+ expect(isZero(input)).toBe(false);
15
+ });
16
+ });
17
+
18
+ it("return false for non-number values", () => {
19
+ const inputs = ["0", "-0", null, undefined, "", {}, [], false, true];
20
+
21
+ expect.assertions(inputs.length);
22
+
23
+ inputs.forEach((input) => {
24
+ expect(isZero(input)).toBe(false);
25
+ });
26
+ });
27
+ });
@@ -0,0 +1,50 @@
1
+ import { requireNumber } from "..";
2
+
3
+ describe("requireNumber", () => {
4
+ it("return number if object key holds a number", () => {
5
+ const inputs = [-1, 1, 100, 0.2];
6
+ expect.assertions(inputs.length);
7
+
8
+ inputs.forEach((input) => {
9
+ const output = requireNumber({ value: input }, "value");
10
+ expect(output).toBe(input);
11
+ });
12
+ });
13
+
14
+ it("return number if object key holds a string as number", () => {
15
+ const inputs = ["-1", "1", "100", "0.2"];
16
+ expect.assertions(inputs.length);
17
+
18
+ inputs.forEach((input) => {
19
+ const output = requireNumber({ value: input }, "value");
20
+ expect(output).toBe(Number(input));
21
+ });
22
+ });
23
+
24
+ it("return null if object key holds zero", () => {
25
+ expect(requireNumber({ value: 0 }, "value")).toBeNull();
26
+ expect(requireNumber({ value: "0" }, "value")).toBeNull();
27
+ });
28
+
29
+ it("return null if object key is missing or not a number", () => {
30
+ const cases = [
31
+ [{}, "value"],
32
+ [{ value: null }, "value"],
33
+ [{ value: undefined }, "value"],
34
+ [{ value: NaN }, "value"],
35
+ [{ value: "" }, "value"],
36
+ [{ value: "vfdvf" }, "value"],
37
+ [{ value: {} }, "value"],
38
+ [{ value: [] }, "value"],
39
+ [null, "value"],
40
+ [undefined, "value"],
41
+ ] as const;
42
+
43
+ expect.assertions(cases.length);
44
+
45
+ cases.forEach(([obj, key]) => {
46
+ const output = requireNumber(obj, key);
47
+ expect(output).toBeNull();
48
+ });
49
+ });
50
+ });
@@ -21,6 +21,33 @@ describe("toNumber", () => {
21
21
  });
22
22
  });
23
23
 
24
+ it("return number if input is boxed string as number", () => {
25
+ const inputs = [
26
+ new String("-1"),
27
+ new String("0"),
28
+ new String("1"),
29
+ Object("100"),
30
+ Object("0.2"),
31
+ ];
32
+
33
+ expect.assertions(inputs.length);
34
+
35
+ inputs.forEach((input) => {
36
+ const output = toNumber(input);
37
+ expect(output).toBe(Number(input));
38
+ });
39
+ });
40
+
41
+ it("return undefined if input is invalid boxed string", () => {
42
+ const inputs = [new String("vfdvf"), new String("5n"), Object("")];
43
+ expect.assertions(inputs.length);
44
+
45
+ inputs.forEach((input) => {
46
+ const output = toNumber(input);
47
+ expect(output).toBeUndefined();
48
+ });
49
+ });
50
+
24
51
  it("return undefined if input is not a number", () => {
25
52
  const inputs = [
26
53
  "vfdvf",
@@ -0,0 +1,64 @@
1
+ import { toNumberWithDefault } from "..";
2
+
3
+ describe("toNumberWithDefault", () => {
4
+ const DEFAULT = 5;
5
+
6
+ it("return number if input is number", () => {
7
+ const inputs = [-1, 0, 1, 100, 0.2];
8
+ expect.assertions(inputs.length);
9
+
10
+ inputs.forEach((input) => {
11
+ const output = toNumberWithDefault(DEFAULT, input);
12
+ expect(output).toBe(input);
13
+ });
14
+ });
15
+
16
+ it("return number if input is string as number", () => {
17
+ const inputs = ["-1", "0", "1", "100", "0.2"];
18
+ expect.assertions(inputs.length);
19
+
20
+ inputs.forEach((input) => {
21
+ const output = toNumberWithDefault(DEFAULT, input);
22
+ expect(output).toBe(Number(input));
23
+ });
24
+ });
25
+
26
+ it("return default if input is not a number", () => {
27
+ const inputs = [
28
+ "vfdvf",
29
+ "5n",
30
+ "-5n",
31
+ null,
32
+ undefined,
33
+ NaN,
34
+ "",
35
+ {},
36
+ { test: 1 },
37
+ [],
38
+ [1],
39
+ ];
40
+
41
+ expect.assertions(inputs.length);
42
+
43
+ inputs.forEach((input) => {
44
+ const output = toNumberWithDefault(DEFAULT, input);
45
+ expect(output).toBe(DEFAULT);
46
+ });
47
+ });
48
+
49
+ describe("BigInt support", () => {
50
+ const isBigIntSupported = typeof BigInt !== "undefined";
51
+
52
+ if (isBigIntSupported) {
53
+ it("converts BigInt to number when BigInt is supported", () => {
54
+ expect(toNumberWithDefault(DEFAULT, BigInt(5))).toBe(5);
55
+ expect(toNumberWithDefault(DEFAULT, BigInt(0))).toBe(0);
56
+ expect(toNumberWithDefault(DEFAULT, BigInt(-10))).toBe(-10);
57
+ });
58
+ } else {
59
+ it("skips BigInt tests when BigInt is not supported", () => {
60
+ expect(typeof BigInt).toBe("undefined");
61
+ });
62
+ }
63
+ });
64
+ });
@@ -0,0 +1,48 @@
1
+ import { toNumberWithDefaultZero } from "..";
2
+
3
+ const DEFAULT = 0;
4
+
5
+ describe("toNumberWithDefaultZero", () => {
6
+ it("return number if input is number", () => {
7
+ const inputs = [-1, 0, 1, 100, 0.2];
8
+ expect.assertions(inputs.length);
9
+
10
+ inputs.forEach((input) => {
11
+ const output = toNumberWithDefaultZero(input);
12
+ expect(output).toBe(input);
13
+ });
14
+ });
15
+
16
+ it("return number if input is string as number", () => {
17
+ const inputs = ["-1", "0", "1", "100", "0.2"];
18
+ expect.assertions(inputs.length);
19
+
20
+ inputs.forEach((input) => {
21
+ const output = toNumberWithDefaultZero(input);
22
+ expect(output).toBe(Number(input));
23
+ });
24
+ });
25
+
26
+ it("return zero if input is not a number", () => {
27
+ const inputs = [
28
+ "vfdvf",
29
+ "5n",
30
+ "-5n",
31
+ null,
32
+ undefined,
33
+ NaN,
34
+ "",
35
+ {},
36
+ { test: 1 },
37
+ [],
38
+ [1],
39
+ ];
40
+
41
+ expect.assertions(inputs.length);
42
+
43
+ inputs.forEach((input) => {
44
+ const output = toNumberWithDefaultZero(input);
45
+ expect(output).toBe(DEFAULT);
46
+ });
47
+ });
48
+ });
@@ -1,18 +1,37 @@
1
- import * as R from "ramda";
1
+ import { isNil } from "@applicaster/zapp-react-native-utils/utils";
2
+
2
3
  import { logger } from "@applicaster/zapp-react-native-utils/logger";
3
4
 
4
5
  const numberUtilsLogger = logger?.addSubsystem("NumberUtils");
5
6
 
7
+ /** Matches Ramda isEmpty: "" / [] / {} are empty; numbers and bigints are not. */
8
+ const isEmpty = (value: unknown): boolean => {
9
+ if (typeof value === "string" || Array.isArray(value)) {
10
+ return value.length === 0;
11
+ }
12
+
13
+ if (typeof value === "object" && value !== null) {
14
+ return Object.keys(value).length === 0;
15
+ }
16
+
17
+ return false;
18
+ };
19
+
6
20
  export const toNumber = (value: unknown): number | undefined => {
7
- if (R.isNil(value) || R.isEmpty(value)) {
21
+ if (isNil(value) || isEmpty(value)) {
8
22
  return undefined;
9
23
  }
10
24
 
11
25
  // Feature-detect BigInt support to avoid ReferenceError on older runtimes
12
26
  const isBigIntSupported = typeof BigInt !== "undefined";
13
- const isBigIntValue = isBigIntSupported && R.is(BigInt, value);
14
-
15
- if (R.is(Number, value) || isBigIntValue || R.is(String, value)) {
27
+ const isBigIntValue = isBigIntSupported && typeof value === "bigint";
28
+
29
+ if (
30
+ typeof value === "number" ||
31
+ isBigIntValue ||
32
+ typeof value === "string" ||
33
+ value instanceof String
34
+ ) {
16
35
  const numberOrNan = Number(value);
17
36
 
18
37
  return Number.isNaN(numberOrNan) ? undefined : numberOrNan;
@@ -59,7 +78,7 @@ export const toFiniteNumberWithDefault = (
59
78
  export const isLikeNumber = (value: unknown): boolean => {
60
79
  const possibleNumber = toNumber(value);
61
80
 
62
- if (R.isNil(possibleNumber)) {
81
+ if (isNil(possibleNumber)) {
63
82
  return false;
64
83
  }
65
84
 
@@ -76,7 +95,7 @@ export const isMinusZero = (value: number) => {
76
95
  export const toPositiveNumber = (value: unknown): number | undefined => {
77
96
  const possibleNumber = toNumber(value);
78
97
 
79
- if (R.isNil(possibleNumber)) {
98
+ if (isNil(possibleNumber)) {
80
99
  return undefined;
81
100
  }
82
101
 
@@ -97,7 +116,7 @@ export const toPositiveNumberWithDefault = (
97
116
  ): number => {
98
117
  const possibleNumber = toNumber(value);
99
118
 
100
- if (R.isNil(possibleNumber)) {
119
+ if (isNil(possibleNumber)) {
101
120
  return defaultValue;
102
121
  }
103
122
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "16.0.0-alpha.2295427024",
3
+ "version": "16.0.0-alpha.2480030155",
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.2295427024",
30
+ "@applicaster/applicaster-types": "16.0.0-alpha.2480030155",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -1,8 +1,12 @@
1
1
  /// <reference types="@applicaster/applicaster-types" />
2
2
  /* eslint-disable @typescript-eslint/no-unused-vars */
3
- import { Context, useContext, useState } from "react";
3
+ import { Context, useCallback, useContext, useEffect, useState } from "react";
4
4
  import { ActionsContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ActionsContext";
5
5
 
6
+ import {
7
+ observeEntryState,
8
+ RegisteredActionValue,
9
+ } from "../../uiActionsRegistrator";
6
10
  import { reactHooksLogger } from "../logger";
7
11
 
8
12
  const logger = reactHooksLogger.addSubsystem("actions");
@@ -46,3 +50,49 @@ export function useActions<T = unknown>(plugId: string): any {
46
50
 
47
51
  return context;
48
52
  }
53
+
54
+ export type UseEntryActionState = {
55
+ state: CellActionEntryState | undefined;
56
+ invokeAction: (options?: InvokeArgsOptions) => void;
57
+ };
58
+
59
+ /**
60
+ * Subscribes a component to a single action's state for a given `entry`.
61
+ *
62
+ * It observes state changes reactively via `observeEntryState` (which is backed
63
+ * by the action's `addListener`), seeds the initial value synchronously from
64
+ * `initialEntryState`, and returns a memoized `invokeAction` that forwards
65
+ * optimistic `updateState` updates back into local state.
66
+ *
67
+ * This is the single, unified way to drive an action button - it works the same
68
+ * for registry actions, legacy context-provider actions and entry actions.
69
+ */
70
+ export function useEntryActionState(
71
+ action: RegisteredActionValue | undefined,
72
+ entry: ZappEntry | ZappFeed
73
+ ): UseEntryActionState {
74
+ const [state, setState] = useState<CellActionEntryState | undefined>(() =>
75
+ action?.initialEntryState?.(entry)
76
+ );
77
+
78
+ const entryId = (entry as ZappEntry)?.id;
79
+
80
+ useEffect(() => {
81
+ if (!action) return undefined;
82
+
83
+ const subscription = observeEntryState(action, entry).subscribe(setState);
84
+
85
+ return () => subscription.unsubscribe();
86
+ // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
87
+ }, [action, entryId]);
88
+
89
+ const invokeAction = useCallback(
90
+ (options: InvokeArgsOptions = {}) => {
91
+ action?.invokeAction?.(entry, { updateState: setState, ...options });
92
+ },
93
+ // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
94
+ [action, entryId]
95
+ );
96
+
97
+ return { state, invokeAction };
98
+ }
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import * as R from "ramda";
5
5
  import { handleActionSchemeUrl } from "@applicaster/quick-brick-core/App/DeepLinking/URLSchemeHandler/SchemeHandlerHooks/useUrlSchemeHandler";
6
6
  import { CellTapContext } from "@applicaster/zapp-react-native-ui-components/Contexts/CellTapContext";
7
+ import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
7
8
  import {
8
9
  useNavigation,
9
10
  useProfilerLogging,
@@ -28,7 +29,6 @@ import { useScreenStateStore } from "../navigation/useScreenStateStore";
28
29
  type Props = {
29
30
  item?: ZappEntry;
30
31
  index?: number;
31
- component?: ZappUIComponent;
32
32
  zappPipesData?: ZappPipesData;
33
33
  };
34
34
 
@@ -37,16 +37,16 @@ type onPressReturnFn =
37
37
  | (() => void);
38
38
 
39
39
  export const useCellClick = ({
40
- component,
41
40
  zappPipesData,
42
41
  item,
43
- }: Props): onPressReturnFn => {
42
+ }: Props = {}): onPressReturnFn => {
44
43
  const { push, currentRoute } = useNavigation();
45
44
  const { pathname } = useRoute();
46
45
  const screenStateStore = useScreenStateStore();
47
46
 
48
47
  const onCellTap: Option<Function> = React.useContext(CellTapContext);
49
48
  const actionExecutor = React.useContext(ActionExecutorContext);
49
+ const component = useUIComponentContext();
50
50
  const screenData = useCurrentScreenData();
51
51
  const screenState = useScreenContext()?.options;
52
52
  const entry = useScreenContext()?.entry;
@@ -242,4 +242,29 @@ describe("useGetUrlInflater", () => {
242
242
  const url = result.current(source, mapping);
243
243
  expect(url).toBe("https://foo.com/shows/A123?q=user%20query");
244
244
  });
245
+
246
+ it("handles search queries with apostrophes without HTML entity escaping or URL truncation", () => {
247
+ const source =
248
+ "https://api.pc-cms.tele.quebec/api/applicaster/v1/mobile/search?q={{q}}";
249
+
250
+ const mapping = {
251
+ q: { source: "search", property: "q" },
252
+ };
253
+
254
+ const searchContext = getSearchContext("L'aventure", mapping);
255
+
256
+ const result = getInflatedDataSourceUrl({
257
+ source,
258
+ contexts: {
259
+ search: searchContext,
260
+ },
261
+ mapping,
262
+ });
263
+
264
+ expect(result).toBe(
265
+ "https://api.pc-cms.tele.quebec/api/applicaster/v1/mobile/search?q=L'aventure"
266
+ );
267
+
268
+ expect(result).not.toContain("&#x27;");
269
+ });
245
270
  });
@@ -49,7 +49,7 @@ export function useStatusBarHeight() {
49
49
  });
50
50
  }
51
51
 
52
- const getNavbarHeight = () => {
52
+ export const getNavbarHeight = () => {
53
53
  return isTV() ? 60 : platformSelect({ ios: 44, android: 56 });
54
54
  };
55
55
 
@@ -38,3 +38,7 @@ export const useNavigationPluginData = jest.fn().mockReturnValue(mockMenu);
38
38
  export const useIsNavBarVisible = jest.fn().mockReturnValue(true);
39
39
 
40
40
  export const useIsScreenActive = jest.fn().mockReturnValue(true);
41
+
42
+ export const isScreenActiveForRoute = jest.fn(
43
+ (route, currentRoute) => route === currentRoute
44
+ );