@applicaster/zapp-react-native-utils 16.0.0-rc.4 → 16.0.0-rc.40

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 (85) hide show
  1. package/actionsExecutor/ActionExecutor.ts +8 -10
  2. package/actionsExecutor/ActionExecutorContext.tsx +43 -326
  3. package/actionsExecutor/actions/appRestart.ts +24 -0
  4. package/actionsExecutor/actions/confirmDialog.ts +53 -0
  5. package/actionsExecutor/actions/index.ts +30 -0
  6. package/actionsExecutor/actions/localStorageRemove.ts +27 -0
  7. package/actionsExecutor/actions/localStorageSet.ts +28 -0
  8. package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
  9. package/actionsExecutor/actions/navigateToScreen.ts +123 -0
  10. package/actionsExecutor/actions/refreshComponent.ts +79 -0
  11. package/actionsExecutor/actions/screenSetVariable.ts +35 -0
  12. package/actionsExecutor/actions/screenToggleFlag.ts +38 -0
  13. package/actionsExecutor/actions/sendCloudEvent.ts +88 -0
  14. package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
  15. package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
  16. package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
  17. package/actionsExecutor/actions/showToast.ts +31 -0
  18. package/actionsExecutor/actions/switchLayout.ts +40 -0
  19. package/actionsExecutor/types.ts +59 -0
  20. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
  21. package/analyticsUtils/PlayerAnalyticsManager.ts +11 -1
  22. package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
  23. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
  24. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
  25. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
  26. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
  27. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
  28. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
  29. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
  30. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
  31. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
  32. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
  33. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
  34. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
  35. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
  36. package/analyticsUtils/__tests__/fixtures/index.js +20 -0
  37. package/analyticsUtils/analyticsMapper.ts +4 -1
  38. package/analyticsUtils/playerAnalyticsTracker.ts +26 -3
  39. package/appUtils/HooksManager/index.ts +3 -8
  40. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -1
  41. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -1
  42. package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
  43. package/appUtils/contextKeysManager/index.ts +1 -1
  44. package/appUtils/contextKeysManager/utils/index.ts +38 -25
  45. package/appUtils/playerManager/__tests__/playerFactory.test.ts +150 -0
  46. package/appUtils/playerManager/playerFactory.ts +17 -34
  47. package/cellUtils/index.ts +3 -5
  48. package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
  49. package/colorUtils/__tests__/isValidColor.test.ts +70 -0
  50. package/colorUtils/index.ts +50 -0
  51. package/manifestUtils/_internals/index.js +6 -0
  52. package/manifestUtils/defaultManifestConfigurations/generalContent.js +35 -0
  53. package/manifestUtils/defaultManifestConfigurations/player.js +15 -15
  54. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
  55. package/manifestUtils/fieldUtils/index.js +125 -0
  56. package/manifestUtils/keys.js +9 -0
  57. package/manifestUtils/mobileAction/button/index.js +16 -0
  58. package/manifestUtils/mobileAction/container/index.js +3 -1
  59. package/manifestUtils/mobileAction/groups/defaults.js +3 -1
  60. package/manifestUtils/platformIsTV.js +1 -0
  61. package/package.json +2 -2
  62. package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
  63. package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
  64. package/pipesUtils/index.ts +1 -0
  65. package/pipesUtils/withPipesEndpoint.tsx +54 -0
  66. package/playerUtils/index.ts +24 -2
  67. package/reactHooks/actions/index.ts +51 -1
  68. package/reactHooks/cell-click/index.ts +18 -10
  69. package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
  70. package/reactHooks/feed/index.ts +5 -1
  71. package/reactHooks/feed/useBatchLoading.ts +9 -1
  72. package/reactHooks/feed/{usePipesCacheReset.ts → useMarkPipesDataStale.ts} +12 -4
  73. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  74. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  75. package/reactHooks/navigation/index.ts +1 -1
  76. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  77. package/reactHooks/screen/__tests__/useIsStandaloneFullscreen.test.ts +114 -0
  78. package/reactHooks/screen/index.ts +2 -0
  79. package/reactHooks/screen/useIsStandaloneFullscreen.ts +12 -0
  80. package/reactHooks/state/useComponentScreenState.ts +1 -1
  81. package/reactHooks/utils/index.ts +3 -2
  82. package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +22 -4
  83. package/riverComponetsMeasurementProvider/index.tsx +12 -8
  84. package/uiActionsRegistrator/index.ts +203 -0
  85. package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
@@ -0,0 +1,76 @@
1
+ import { isTransparentColor } from "..";
2
+
3
+ describe("isTransparentColor", () => {
4
+ it("returns true for transparent keyword and rgba colors with zero alpha", () => {
5
+ const transparentColors = [
6
+ "transparent",
7
+ "rgba(0,0,0,0)",
8
+ "rgba(255, 255, 255, 0)",
9
+ "rgba(0,0,0,0.0)",
10
+ "rgba(0,0,0,0.00)",
11
+ "rgba(0,0,0, 0)",
12
+ "hsla(0,0%,0%,0)",
13
+ ];
14
+
15
+ expect.assertions(transparentColors.length);
16
+
17
+ transparentColors.forEach((color) => {
18
+ expect(isTransparentColor(color)).toBe(true);
19
+ });
20
+ });
21
+
22
+ it("returns false for valid colors that are not fully transparent", () => {
23
+ const nonTransparentColors = [
24
+ "red",
25
+ "#fff",
26
+ "#ffffff",
27
+ "rgb(0,0,0)",
28
+ "rgba(0,0,0)",
29
+ "rgba(0,0,0,0.5)",
30
+ "rgba(255, 255, 255, 0.3)",
31
+ "rgba(255, 255, 255, 1.0)",
32
+ "rgba(239,239,239,1.0)",
33
+ "currentColor",
34
+ "inherit",
35
+ "#00000000",
36
+ "#fff0",
37
+ ];
38
+
39
+ expect.assertions(nonTransparentColors.length);
40
+
41
+ nonTransparentColors.forEach((color) => {
42
+ expect(isTransparentColor(color)).toBe(false);
43
+ });
44
+ });
45
+
46
+ it("returns false for case variants of the transparent keyword", () => {
47
+ expect(isTransparentColor("Transparent")).toBe(false);
48
+ expect(isTransparentColor("TRANSPARENT")).toBe(false);
49
+ });
50
+
51
+ it("returns false for invalid color strings", () => {
52
+ const invalidColors = [
53
+ "invalid",
54
+ "",
55
+ "#gggggg",
56
+ "#fff.00",
57
+ "unset",
58
+ " rgba(0,0,0,0) ",
59
+ "transparent ",
60
+ ];
61
+
62
+ expect.assertions(invalidColors.length);
63
+
64
+ invalidColors.forEach((color) => {
65
+ expect(isTransparentColor(color)).toBe(false);
66
+ });
67
+ });
68
+
69
+ it("returns false for nullish and non-string values", () => {
70
+ expect(isTransparentColor(undefined)).toBe(false);
71
+ expect(isTransparentColor(null)).toBe(false);
72
+ expect(isTransparentColor(123)).toBe(false);
73
+ expect(isTransparentColor({})).toBe(false);
74
+ expect(isTransparentColor([])).toBe(false);
75
+ });
76
+ });
@@ -0,0 +1,70 @@
1
+ import { isValidColor } from "..";
2
+
3
+ describe("isValidColor", () => {
4
+ it("returns true for valid hex colors", () => {
5
+ const validHexColors = ["#fff", "#ffffff", "#FF0000", "#000", "#abc123"];
6
+
7
+ expect.assertions(validHexColors.length);
8
+
9
+ validHexColors.forEach((color) => {
10
+ expect(isValidColor(color)).toBe(true);
11
+ });
12
+ });
13
+
14
+ it("returns true for valid rgb and rgba colors", () => {
15
+ const validRgbColors = [
16
+ "rgb(255,0,0)",
17
+ "rgb(255, 0, 0)",
18
+ "rgba(255,0,0,0.5)",
19
+ "rgba(255, 0, 0, 0.5)",
20
+ ];
21
+
22
+ expect.assertions(validRgbColors.length);
23
+
24
+ validRgbColors.forEach((color) => {
25
+ expect(isValidColor(color)).toBe(true);
26
+ });
27
+ });
28
+
29
+ it("returns true for valid named and special CSS colors", () => {
30
+ const validNamedColors = ["red", "transparent", "currentColor", "inherit"];
31
+
32
+ expect.assertions(validNamedColors.length);
33
+
34
+ validNamedColors.forEach((color) => {
35
+ expect(isValidColor(color)).toBe(true);
36
+ });
37
+ });
38
+
39
+ it("returns true for valid hsl colors", () => {
40
+ expect(isValidColor("hsl(0, 100%, 50%)")).toBe(true);
41
+ });
42
+
43
+ it("returns false for invalid color strings", () => {
44
+ const invalidColors = [
45
+ "invalid",
46
+ "",
47
+ "#gggggg",
48
+ "#fff.00",
49
+ "unset",
50
+ undefined,
51
+ null,
52
+ ];
53
+
54
+ expect.assertions(invalidColors.length);
55
+
56
+ invalidColors.forEach((color) => {
57
+ expect(isValidColor(color)).toBe(false);
58
+ });
59
+ });
60
+
61
+ it("returns true for valid rgb and rgba colors", () => {
62
+ const validRgbColors = ["rgba(239,239,239,1.0)"];
63
+
64
+ expect.assertions(validRgbColors.length);
65
+
66
+ validRgbColors.forEach((color) => {
67
+ expect(isValidColor(color)).toBe(true);
68
+ });
69
+ });
70
+ });
@@ -0,0 +1,50 @@
1
+ import validateColor from "validate-color";
2
+ import { isNil } from "@applicaster/zapp-react-native-utils/utils";
3
+ import { isString } from "@applicaster/zapp-react-native-utils/stringUtils";
4
+
5
+ const normalizeRgbaAlpha = (color: string): string => {
6
+ return color.replace(
7
+ /^rgba\(([^)]+),\s*(\d+(?:\.\d+)?)\s*\)$/i,
8
+ (_, rgb, alpha) => {
9
+ const alphaValue = parseFloat(alpha);
10
+
11
+ if (!Number.isFinite(alphaValue) || !Number.isInteger(alphaValue)) {
12
+ return color;
13
+ }
14
+
15
+ return `rgba(${rgb},${alphaValue})`;
16
+ }
17
+ );
18
+ };
19
+
20
+ export const isValidColor = (color: string): boolean => {
21
+ if (isNil(color) || !isString(color)) {
22
+ return false;
23
+ }
24
+
25
+ if (validateColor(color)) {
26
+ return true;
27
+ }
28
+
29
+ // validate-color rejects integer alpha values written as floats (e.g. 1.0)
30
+ // https://github.com/dreamyguy/validate-color/issues/44
31
+ const normalizedColor = normalizeRgbaAlpha(color);
32
+
33
+ return normalizedColor !== color && validateColor(normalizedColor);
34
+ };
35
+
36
+ function isRgbaAlphaZero(color: string): boolean {
37
+ const layers = color
38
+ .replace("rgba(", "")
39
+ .replace(")", "")
40
+ .split(",")
41
+ .map((layer) => layer.trim());
42
+
43
+ return Number(layers[3]) === 0;
44
+ }
45
+
46
+ export const isTransparentColor = (color: string): boolean => {
47
+ return (
48
+ isValidColor(color) && (color === "transparent" || isRgbaAlphaZero(color))
49
+ );
50
+ };
@@ -157,6 +157,10 @@ function generateFieldsFromDefaultsWithoutPrefixedLabel(
157
157
 
158
158
  if (conditions) {
159
159
  generatedField.conditional_fields = conditions.map((condition) => {
160
+ if (condition.section === null) {
161
+ return { key: condition.key, condition_value: condition.value };
162
+ }
163
+
160
164
  const section = condition.section ? `${condition.section}/` : "";
161
165
 
162
166
  return {
@@ -164,6 +168,8 @@ function generateFieldsFromDefaultsWithoutPrefixedLabel(
164
168
  condition_value: condition.value,
165
169
  };
166
170
  });
171
+
172
+ generatedField.rules = "all_conditions";
167
173
  }
168
174
 
169
175
  return generatedField;
@@ -308,6 +308,41 @@ const generalContent = () => ({
308
308
  key: "pull_to_refresh_enabled",
309
309
  initial_value: false,
310
310
  },
311
+ {
312
+ type: "switch",
313
+ label: "Allow using this screen as a hook",
314
+ label_tooltip:
315
+ "Make sure that screen uses 'finishHook' action or performs navigation action to exit the screen after performing the hook action, or user will be stuck on the screen", // eslint-disable-line max-len
316
+ key: "available_as_hook",
317
+ initial_value: false,
318
+ },
319
+ {
320
+ type: "data_source_selector",
321
+ label: "Skip hook endpoint",
322
+ key: "skip_hook_endpoint",
323
+ label_tooltip:
324
+ "If set, this endpoint will check with the server whether the hook should be skipped",
325
+ conditional_fields: [
326
+ {
327
+ key: "rules/available_as_hook",
328
+ condition_value: true,
329
+ },
330
+ ],
331
+ },
332
+ {
333
+ key: "skip_hook_storage_key",
334
+ type: "text_input",
335
+ label: "Hook will be skipped if storage key is set",
336
+ initial_value: "",
337
+ label_tooltip:
338
+ "Comma-separated keys in namespace.key format (key without a dot uses the default namespace)", // eslint-disable-line max-len
339
+ conditional_fields: [
340
+ {
341
+ key: "rules/available_as_hook",
342
+ condition_value: true,
343
+ },
344
+ ],
345
+ },
311
346
  ],
312
347
  },
313
348
  });
@@ -781,21 +781,6 @@ function getPlayerConfiguration({ platform, version }) {
781
781
  }
782
782
  );
783
783
 
784
- if (
785
- platform.includes("android") ||
786
- platform.includes("amazon") ||
787
- platform.includes("ios") ||
788
- platform.includes("tvos")
789
- ) {
790
- general.fields.push({
791
- key: "user_agent",
792
- label: "User agent",
793
- initial_value: "",
794
- type: "text_input",
795
- label_tooltip: "Override default user agent string",
796
- });
797
- }
798
-
799
784
  if (platform === "android_for_quickbrick") {
800
785
  general.fields.push({
801
786
  key: "stop_playback_on_task_removal",
@@ -2415,6 +2400,21 @@ function getPlayerConfiguration({ platform, version }) {
2415
2400
  );
2416
2401
  }
2417
2402
 
2403
+ if (
2404
+ platform.includes("android") ||
2405
+ platform.includes("amazon") ||
2406
+ platform.includes("ios") ||
2407
+ platform.includes("tvos")
2408
+ ) {
2409
+ general.fields.push({
2410
+ key: "user_agent",
2411
+ label: "User agent",
2412
+ initial_value: "",
2413
+ type: "text_input",
2414
+ label_tooltip: "Override default user agent string",
2415
+ });
2416
+ }
2417
+
2418
2418
  compact([
2419
2419
  {
2420
2420
  key: "liveCatchUpEnabled",
@@ -1,9 +1,93 @@
1
1
  const {
2
+ parseShorthand,
3
+ margin,
4
+ padding,
2
5
  withConditional,
3
6
  getConditionalKey,
4
7
  createConditionalField,
5
8
  } = require("..");
6
9
 
10
+ describe("parseShorthand", () => {
11
+ it("1 value → all four sides equal", () => {
12
+ expect(parseShorthand("8")).toEqual([8, 8, 8, 8]);
13
+ });
14
+
15
+ it("2 values → top/bottom, right/left", () => {
16
+ expect(parseShorthand("0 20")).toEqual([0, 20, 0, 20]);
17
+ });
18
+
19
+ it("3 values → top, right/left, bottom", () => {
20
+ expect(parseShorthand("0 0 20")).toEqual([0, 0, 20, 0]);
21
+ });
22
+
23
+ it("4 values → top right bottom left", () => {
24
+ expect(parseShorthand("1 2 3 4")).toEqual([1, 2, 3, 4]);
25
+ });
26
+
27
+ it("throws on invalid count", () => {
28
+ expect(() => parseShorthand("1 2 3 4 5")).toThrow();
29
+ });
30
+ });
31
+
32
+ describe("margin", () => {
33
+ it("returns 4 field descriptors with correct suffixes", () => {
34
+ const fields = margin("margin", { mobile: "0" });
35
+
36
+ expect(fields.map((f) => f.suffix)).toEqual([
37
+ "margin top",
38
+ "margin right",
39
+ "margin bottom",
40
+ "margin left",
41
+ ]);
42
+ });
43
+
44
+ it("sets type to number_input", () => {
45
+ margin("margin", { mobile: "0" }).forEach((f) => {
46
+ expect(f.type).toBe("number_input");
47
+ });
48
+ });
49
+
50
+ it("expands shorthand per platform", () => {
51
+ const fields = margin("margin", { mobile: "0 0 20", tv: "0 0 90" });
52
+ const byKey = Object.fromEntries(fields.map((f) => [f.suffix, f]));
53
+ expect(byKey["margin top"].initialValue).toEqual({ mobile: 0, tv: 0 });
54
+ expect(byKey["margin bottom"].initialValue).toEqual({ mobile: 20, tv: 90 });
55
+ expect(byKey["margin left"].initialValue).toEqual({ mobile: 0, tv: 0 });
56
+ });
57
+
58
+ it("supports a custom suffix prefix", () => {
59
+ const fields = margin("input margin", { mobile: "4" });
60
+ expect(fields[0].suffix).toBe("input margin top");
61
+ });
62
+ });
63
+
64
+ describe("padding", () => {
65
+ it("returns 4 field descriptors with correct suffixes", () => {
66
+ const fields = padding("padding", { mobile: "0" });
67
+
68
+ expect(fields.map((f) => f.suffix)).toEqual([
69
+ "padding top",
70
+ "padding right",
71
+ "padding bottom",
72
+ "padding left",
73
+ ]);
74
+ });
75
+
76
+ it("expands 2-value shorthand symmetrically", () => {
77
+ const fields = padding("padding", { mobile: "0 20", tv: "0 124" });
78
+ const byKey = Object.fromEntries(fields.map((f) => [f.suffix, f]));
79
+
80
+ expect(byKey["padding right"].initialValue).toEqual({
81
+ mobile: 20,
82
+ tv: 124,
83
+ });
84
+
85
+ expect(byKey["padding left"].initialValue).toEqual({ mobile: 20, tv: 124 });
86
+ expect(byKey["padding top"].initialValue).toEqual({ mobile: 0, tv: 0 });
87
+ expect(byKey["padding bottom"].initialValue).toEqual({ mobile: 0, tv: 0 });
88
+ });
89
+ });
90
+
7
91
  describe("manifestUtils/fieldUtils", () => {
8
92
  it("appends conditions and adds all_conditions when there is more than one condition", () => {
9
93
  const config = {
@@ -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,
@@ -43,6 +43,7 @@ const TV_PLATFORMS = [
43
43
  "tvos_for_quickbrick",
44
44
  "samsung_tv",
45
45
  "lg_tv",
46
+ "vidaa",
46
47
  "vizio",
47
48
  ];
48
49
 
@@ -630,6 +631,14 @@ const MOBILE_ACTION_BUTTON_FIELDS = [
630
631
  type: ZAPPIFEST_FIELDS.number_input,
631
632
  suffix: "margin left",
632
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
+ },
633
642
  ];
634
643
 
635
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 = {
@@ -4,6 +4,7 @@ function platformIsTV(platform) {
4
4
  "android_tv_for_quickbrick",
5
5
  "amazon_fire_tv_for_quickbrick",
6
6
  "lg_tv",
7
+ "vidaa",
7
8
  "samsung_tv",
8
9
  "web",
9
10
  "vizio",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "16.0.0-rc.4",
3
+ "version": "16.0.0-rc.40",
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-rc.4",
30
+ "@applicaster/applicaster-types": "16.0.0-rc.40",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -0,0 +1,33 @@
1
+ import { buildUrlWithQuery } from "../withPipesEndpoint";
2
+
3
+ describe("buildUrlWithQuery", () => {
4
+ it("returns the url unchanged when requestParams is null", () => {
5
+ expect(buildUrlWithQuery("https://foo.com/path", null)).toBe(
6
+ "https://foo.com/path"
7
+ );
8
+ });
9
+
10
+ it("returns the url unchanged when requestParams has no params", () => {
11
+ expect(buildUrlWithQuery("https://foo.com/path", { headers: {} })).toBe(
12
+ "https://foo.com/path"
13
+ );
14
+ });
15
+
16
+ it("returns the url unchanged when url is empty", () => {
17
+ expect(buildUrlWithQuery("", { params: { a: "1" } })).toBe("");
18
+ });
19
+
20
+ it("appends params to a url with no existing query", () => {
21
+ expect(
22
+ buildUrlWithQuery("https://foo.com/path", { params: { token: "abc" } })
23
+ ).toBe("https://foo.com/path?token=abc");
24
+ });
25
+
26
+ it("merges params with an existing query string", () => {
27
+ expect(
28
+ buildUrlWithQuery("https://foo.com/path?existing=1", {
29
+ params: { token: "abc" },
30
+ })
31
+ ).toBe("https://foo.com/path?existing=1&token=abc");
32
+ });
33
+ });