@applicaster/zapp-react-native-utils 16.0.0-alpha.9183418480 → 16.0.0-alpha.9225550512

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 (80) 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/cellUtils/index.ts +3 -5
  46. package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
  47. package/colorUtils/__tests__/isValidColor.test.ts +70 -0
  48. package/colorUtils/index.ts +50 -0
  49. package/manifestUtils/_internals/index.js +6 -0
  50. package/manifestUtils/defaultManifestConfigurations/generalContent.js +35 -0
  51. package/manifestUtils/defaultManifestConfigurations/player.js +15 -15
  52. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
  53. package/manifestUtils/fieldUtils/index.js +125 -0
  54. package/manifestUtils/keys.js +9 -0
  55. package/manifestUtils/mobileAction/button/index.js +16 -0
  56. package/manifestUtils/mobileAction/container/index.js +3 -1
  57. package/manifestUtils/mobileAction/groups/defaults.js +3 -1
  58. package/manifestUtils/platformIsTV.js +1 -0
  59. package/package.json +2 -2
  60. package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
  61. package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
  62. package/pipesUtils/index.ts +1 -0
  63. package/pipesUtils/withPipesEndpoint.tsx +54 -0
  64. package/playerUtils/index.ts +4 -2
  65. package/reactHooks/actions/index.ts +51 -1
  66. package/reactHooks/cell-click/index.ts +18 -10
  67. package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
  68. package/reactHooks/feed/index.ts +5 -1
  69. package/reactHooks/feed/useBatchLoading.ts +9 -1
  70. package/reactHooks/feed/{usePipesCacheReset.ts → useMarkPipesDataStale.ts} +12 -4
  71. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  72. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  73. package/reactHooks/navigation/index.ts +1 -1
  74. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  75. package/reactHooks/state/useComponentScreenState.ts +1 -1
  76. package/reactHooks/utils/index.ts +3 -2
  77. package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +22 -4
  78. package/riverComponetsMeasurementProvider/index.tsx +12 -8
  79. package/uiActionsRegistrator/index.ts +203 -0
  80. package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
@@ -2,17 +2,11 @@
2
2
  {
3
3
  "first": {
4
4
  "name": "Screen viewed: Ads & redirects",
5
- "params": {
6
- "ignored_param": "removed_value",
7
- "kept_param": "kept_value"
8
- }
5
+ "params": {}
9
6
  },
10
7
  "second": {
11
8
  "name": "Screen Ads & redirects",
12
- "params": {
13
- "ignored_param": "removed_value",
14
- "kept_param": "kept_value"
15
- }
9
+ "params": {}
16
10
  }
17
11
  }
18
- ]
12
+ ]
@@ -0,0 +1,18 @@
1
+ [
2
+ {
3
+ "first": {
4
+ "name": "screen_view",
5
+ "params": {}
6
+ },
7
+ "second": {
8
+ "name": "page_view",
9
+ "params": {}
10
+ }
11
+ },
12
+ {
13
+ "first": {
14
+ "name": "player_presented",
15
+ "params": {}
16
+ }
17
+ }
18
+ ]
@@ -0,0 +1,16 @@
1
+ {
2
+ "rules": [
3
+ {
4
+ "event": "player_presented",
5
+ "ignore": true,
6
+ "strategy": "allowUnlisted"
7
+ },
8
+ {
9
+ "event": "screen_view",
10
+ "ignore": false,
11
+ "rename": "page_view",
12
+ "strategy": "allowUnlisted"
13
+ }
14
+ ],
15
+ "strategy": "allowUnlisted"
16
+ }
@@ -0,0 +1,4 @@
1
+ [
2
+ { "first": { "name": "my_event", "params": {} },
3
+ "second": { "name": "my_event", "params": {} } }
4
+ ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "rules": [
3
+ { "event": "my_event", "ignore": false, "rename": "should_not_apply", "regex": "wont_match_.*", "strategy": "allowUnlisted" }
4
+ ],
5
+ "strategy": "allowUnlisted"
6
+ }
@@ -0,0 +1,4 @@
1
+ [
2
+ { "first": { "name": "play-video", "params": {} },
3
+ "second": { "name": "play_video", "params": {} } }
4
+ ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "rules": [
3
+ { "ignore": false, "rename": "$<first>_$<second>", "regex": "(?<first>[a-z]+)-(?<second>[a-z]+)", "strategy": "allowUnlisted" }
4
+ ],
5
+ "strategy": "allowUnlisted"
6
+ }
@@ -0,0 +1,4 @@
1
+ [
2
+ { "first": { "name": "screen_view", "params": {} },
3
+ "second": { "name": "renamed", "params": {} } }
4
+ ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "rules": [
3
+ { "ignore": false, "rename": "renamed", "regex": "screen_view", "strategy": "allowUnlisted" }
4
+ ],
5
+ "strategy": "allowUnlisted"
6
+ }
@@ -14,6 +14,16 @@ import blockUnlistedEventEvents from "./analytics_mapper_testBlockUnlistedEvents
14
14
  import blockUnlistedEventRules from "./analytics_mapper_testBlockUnlistedEvents_rules.json";
15
15
  import ACPEventEvents from "./analytics_mapper_testACP_events.json";
16
16
  import ACPEventRules from "./analytics_mapper_testACP_rules.json";
17
+ import emptyRenameRules from "./analytics_mapper_testEmptyRenameKeepsName_rules.json";
18
+ import emptyRenameEvents from "./analytics_mapper_testEmptyRenameKeepsName_events.json";
19
+ import ignoreOrderingRules from "./analytics_mapper_testIgnoreOrdering_rules.json";
20
+ import ignoreOrderingEvents from "./analytics_mapper_testIgnoreOrdering_events.json";
21
+ import regexEventNoFallbackRules from "./analytics_mapper_testRegexEventNoFallback_rules.json";
22
+ import regexEventNoFallbackEvents from "./analytics_mapper_testRegexEventNoFallback_events.json";
23
+ import regexMultiGroupRenameRules from "./analytics_mapper_testRegexMultiGroupRename_rules.json";
24
+ import regexMultiGroupRenameEvents from "./analytics_mapper_testRegexMultiGroupRename_events.json";
25
+ import regexNonNamedFullMatchRules from "./analytics_mapper_testRegexNonNamedFullMatch_rules.json";
26
+ import regexNonNamedFullMatchEvents from "./analytics_mapper_testRegexNonNamedFullMatch_events.json";
17
27
 
18
28
  export const fixtures = {
19
29
  renameRules,
@@ -32,4 +42,14 @@ export const fixtures = {
32
42
  blockUnlistedEventRules,
33
43
  ACPEventEvents,
34
44
  ACPEventRules,
45
+ emptyRenameRules,
46
+ emptyRenameEvents,
47
+ ignoreOrderingRules,
48
+ ignoreOrderingEvents,
49
+ regexEventNoFallbackRules,
50
+ regexEventNoFallbackEvents,
51
+ regexMultiGroupRenameRules,
52
+ regexMultiGroupRenameEvents,
53
+ regexNonNamedFullMatchRules,
54
+ regexNonNamedFullMatchEvents,
35
55
  };
@@ -76,7 +76,10 @@ class StateHolder {
76
76
  delete this.pluckedParams[prefix];
77
77
  }
78
78
 
79
- return this._cap?.[suffix];
79
+ // Stringify so values match the native iOS/Android mappers (which coerce
80
+ // analyticsCustomProperties values to strings) — keeps GA4 data consistent
81
+ // across web/LG/Samsung and native platforms.
82
+ return this._cap?.[suffix]?.toString() ?? null;
80
83
  }
81
84
 
82
85
  private async getFromLocalAndSession(
@@ -4,8 +4,14 @@ import { isTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
4
4
  import { postAnalyticEvent } from "./manager";
5
5
  import { isLive } from "../playerUtils";
6
6
  import { extensionsEvents } from "./AnalyticsEvents/helper";
7
- import { AD_EVENT, EVENT_TYPES, PLAYER_DISPLAY_STATES } from "./events";
7
+ import {
8
+ AD_EVENT,
9
+ EVENT_TYPES,
10
+ GENERAL_EVENT,
11
+ PLAYER_DISPLAY_STATES,
12
+ } from "./events";
8
13
  import { Player } from "../appUtils/playerManager/player";
14
+ import { isNil } from "@applicaster/zapp-react-native-utils/utils";
9
15
 
10
16
  export interface PlayerAnalyticsTrackerI {
11
17
  handleAnalyticEvent(
@@ -13,6 +19,8 @@ export interface PlayerAnalyticsTrackerI {
13
19
  eventData?: QuickBrickPlayer.EventData
14
20
  ): void;
15
21
  setPlayer(player: Player): void;
22
+ onPlayerPresented(): void;
23
+ onPlayerClosed(): void;
16
24
  }
17
25
 
18
26
  export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
@@ -26,6 +34,15 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
26
34
  this.player = player;
27
35
  }
28
36
 
37
+ onPlayerPresented() {
38
+ this.handleAnalyticEvent(GENERAL_EVENT.player_presented);
39
+ }
40
+
41
+ onPlayerClosed() {
42
+ this.handleAnalyticEvent(GENERAL_EVENT.session_end);
43
+ this.handleAnalyticEvent(GENERAL_EVENT.player_closed);
44
+ }
45
+
29
46
  isAdBreak(event: string): boolean {
30
47
  const adBreakEvents = [
31
48
  AD_EVENT.ad_break_start,
@@ -180,7 +197,8 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
180
197
  this.entry?.extensions?.free || this.entry?.extensions?.isFree;
181
198
 
182
199
  const title = this.entry?.title;
183
- const streamType = isLive(this.entry) ? "live" : "vod"; // Todo: determine other types, channel, podcast, aod
200
+ const isStreamLive = isLive(this.entry);
201
+ const streamType = isStreamLive ? "live" : "vod"; // Todo: determine other types, channel, podcast, aod
184
202
 
185
203
  const currentPosition = this.getCurrentPosition(
186
204
  event,
@@ -201,6 +219,7 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
201
219
  );
202
220
 
203
221
  const playerState = this.getPlayerState();
222
+ const playerType = this.player?.playerPluginId;
204
223
 
205
224
  const analyticsCustomProperties = extensionsEvents(this.entry?.extensions);
206
225
 
@@ -210,7 +229,11 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
210
229
  name: title,
211
230
  media_type: mediaType,
212
231
  stream_type: streamType,
213
- duration: mediaDuration,
232
+ player_type: playerType,
233
+ // We do not pass duration for live streams anymore (it's now seekableDuration),
234
+ // but we can't mark it as optional in Analytics Mapper yet,
235
+ // so we set it to -1 for live streams
236
+ duration: isStreamLive && isNil(mediaDuration) ? -1 : mediaDuration,
214
237
  current_position: currentPosition,
215
238
  player_state: playerState,
216
239
  stream_format: contentType,
@@ -87,7 +87,7 @@ export function HooksManager({
87
87
  }: HookManagerArgs): HookManager {
88
88
  hooksManagerLogger.addContext({ targetScreenId: targetScreen.id });
89
89
 
90
- function logHookEvent(func, message, data) {
90
+ function logHookEvent(func, message, data = {}) {
91
91
  func({
92
92
  message,
93
93
  data: __DEV__ ? data : null,
@@ -344,7 +344,6 @@ export function HooksManager({
344
344
  `hookCallback: send app to background, cancelled flow blocker hook ${hookPlugin.identifier} on home screen`,
345
345
  {
346
346
  payload,
347
- hook: hookPlugin,
348
347
  }
349
348
  );
350
349
 
@@ -356,17 +355,13 @@ export function HooksManager({
356
355
  `hookCallback: hook successfully finished: ${hookPlugin.identifier}`,
357
356
  {
358
357
  payload,
359
- hook: hookPlugin,
360
358
  }
361
359
  );
362
360
 
363
361
  if (!callback) {
364
362
  logHookEvent(
365
- hooksManagerLogger.warn,
366
- `hookCallback: ${hookPlugin.identifier} is missing \`callback\`, using hookCallback(default one)`,
367
- {
368
- hookPlugin,
369
- }
363
+ hooksManagerLogger.debug,
364
+ `hookCallback: ${hookPlugin.identifier} is missing \`callback\`, using hookCallback(default one)`
370
365
  );
371
366
 
372
367
  callback = hookCallback;
@@ -46,7 +46,7 @@ describe("Context Keys Manager - getKey", () => {
46
46
 
47
47
  expect(mockedLogger.warn).toHaveBeenCalledWith({
48
48
  message:
49
- "Provided key is not valid - expecing an object with namespace and key properties",
49
+ "Provided key is not valid - expecting an object with namespace and key properties",
50
50
  data: { key: invalidKey },
51
51
  });
52
52
 
@@ -47,7 +47,7 @@ describe("Context Keys Manager - removeKey", () => {
47
47
 
48
48
  expect(mockedLogger.warn).toHaveBeenCalledWith({
49
49
  message:
50
- "Provided key is not valid - expecing an object with namespace and key properties",
50
+ "Provided key is not valid - expecting an object with namespace and key properties",
51
51
  data: { key },
52
52
  });
53
53
 
@@ -48,7 +48,7 @@ describe("Context Keys Manager - setKey", () => {
48
48
 
49
49
  expect(mockedLogger.warn).toHaveBeenCalledWith({
50
50
  message:
51
- "Provided key is not valid - expecing an object with namespace and key properties",
51
+ "Provided key is not valid - expecting an object with namespace and key properties",
52
52
  data: { key },
53
53
  });
54
54
 
@@ -105,7 +105,7 @@ describe("Context Keys Manager - setKey", () => {
105
105
 
106
106
  expect(mockedLogger.warn).toHaveBeenCalledWith({
107
107
  message:
108
- "Provided key is not valid - expecing an object with namespace and key properties",
108
+ "Provided key is not valid - expecting an object with namespace and key properties",
109
109
  data: { key },
110
110
  });
111
111
 
@@ -162,7 +162,7 @@ describe("Context Keys Manager - setKey", () => {
162
162
 
163
163
  expect(mockedLogger.warn).toHaveBeenCalledWith({
164
164
  message:
165
- "Provided key is not valid - expecing an object with namespace and key properties",
165
+ "Provided key is not valid - expecting an object with namespace and key properties",
166
166
  data: { key },
167
167
  });
168
168
 
@@ -217,7 +217,7 @@ describe("Context Keys Manager - setKey", () => {
217
217
 
218
218
  expect(mockedLogger.warn).toHaveBeenCalledWith({
219
219
  message:
220
- "Provided key is not valid - expecing an object with namespace and key properties",
220
+ "Provided key is not valid - expecting an object with namespace and key properties",
221
221
  data: { key },
222
222
  });
223
223
 
@@ -109,7 +109,7 @@ export class ContextKeysManager {
109
109
  if (!keyIsValid(parsedKey)) {
110
110
  this._logger.warn({
111
111
  message:
112
- "Provided key is not valid - expecing an object with namespace and key properties",
112
+ "Provided key is not valid - expecting an object with namespace and key properties",
113
113
  data: { key },
114
114
  });
115
115
 
@@ -1,40 +1,53 @@
1
- import * as R from "ramda";
2
1
  import { DEFAULT_NAMESPACE } from "../consts";
3
2
  import { KeyName, KeyNameObj } from "../index";
4
3
 
5
- const lastDotRegex = /\.([^.]+)$/;
6
- const splitByLastDot = R.compose(R.init, R.split(lastDotRegex));
7
-
8
4
  export function getNamespaceAndKey(namespacedKey: KeyName): KeyNameObj {
9
5
  if (typeof namespacedKey !== "string") return namespacedKey;
10
6
 
11
- const namespacedKeyProps = namespacedKey.includes(".")
12
- ? splitByLastDot(namespacedKey)
13
- : [DEFAULT_NAMESPACE, namespacedKey];
7
+ if (!namespacedKey.includes(".")) {
8
+ return { namespace: DEFAULT_NAMESPACE, key: namespacedKey };
9
+ }
10
+
11
+ // split on the last dot - the namespace itself may contain dots
12
+ const lastDotIndex = namespacedKey.lastIndexOf(".");
14
13
 
15
- return R.zipObj(["namespace", "key"], namespacedKeyProps);
14
+ return {
15
+ namespace: namespacedKey.slice(0, lastDotIndex),
16
+ key: namespacedKey.slice(lastDotIndex + 1),
17
+ };
16
18
  }
17
19
 
18
- const propIsNotNullOrUndefined = (prop) =>
19
- R.compose(R.not, R.isNil, R.prop(prop));
20
+ const hasOwn = (obj: object, prop: string): boolean =>
21
+ Object.prototype.hasOwnProperty.call(obj, prop);
22
+
23
+ const isEmpty = (value: unknown): boolean => {
24
+ if (typeof value === "string" || Array.isArray(value)) {
25
+ return value.length === 0;
26
+ }
27
+
28
+ if (value !== null && typeof value === "object") {
29
+ return Object.keys(value).length === 0;
30
+ }
31
+
32
+ return false;
33
+ };
34
+
35
+ export const keyIsValid = (parsedKey: unknown): boolean => {
36
+ if (parsedKey == null) return false;
37
+ if (typeof parsedKey === "string") return false;
38
+ if (Array.isArray(parsedKey)) return false;
20
39
 
21
- const propIsNotEmpty = (prop) => R.compose(R.not, R.isEmpty, R.prop(prop));
40
+ const obj = parsedKey as Record<string, unknown>;
22
41
 
23
- export const keyIsValid = R.allPass([
24
- R.compose(R.not, R.isNil),
25
- R.compose(R.not, R.is(String)),
26
- R.compose(R.not, R.is(Array)),
27
- R.has("key"),
28
- R.has("namespace"),
29
- propIsNotNullOrUndefined("key"),
30
- propIsNotNullOrUndefined("namespace"),
31
- propIsNotEmpty("key"),
32
- propIsNotEmpty("namespace"),
33
- ]);
42
+ if (!hasOwn(obj, "key") || !hasOwn(obj, "namespace")) return false;
43
+ if (obj.key == null || obj.namespace == null) return false;
44
+ if (isEmpty(obj.key) || isEmpty(obj.namespace)) return false;
45
+
46
+ return true;
47
+ };
34
48
 
35
49
  export const buildNamespaceKey = (key: string, namespace: string) =>
36
50
  `${namespace}.${key}`;
37
51
 
38
- export const savingResultIsSuccess = (result: unknown): boolean => {
39
- return R.is(Boolean, result) && !!result;
40
- };
52
+ export const savingResultIsSuccess = (result: unknown): boolean =>
53
+ typeof result === "boolean" && result === true;
@@ -1,7 +1,7 @@
1
1
  import * as R from "ramda";
2
2
  import { dayjs } from "../dateUtils";
3
- import validateColor from "validate-color";
4
3
 
4
+ import { isValidColor } from "@applicaster/zapp-react-native-utils/colorUtils";
5
5
  import { transformColorCode as fixColorHexCode } from "@applicaster/zapp-react-native-utils/transform";
6
6
  import {
7
7
  capitalize,
@@ -468,15 +468,13 @@ export const getColorFromData = ({
468
468
  data,
469
469
  valueFromLayout,
470
470
  }: GetColorFromData): string => {
471
- // Temporary hack to fix color validation when alpha is floating point number
472
- // https://github.com/dreamyguy/validate-color/issues/44
473
- if (validateColor(valueFromLayout.replace(".00", ""))) {
471
+ if (isValidColor(valueFromLayout)) {
474
472
  return valueFromLayout;
475
473
  }
476
474
 
477
475
  const pathValue = R.path(valueFromLayout.split("."), data);
478
476
 
479
- if (pathValue && validateColor(pathValue)) {
477
+ if (pathValue && isValidColor(pathValue)) {
480
478
  return pathValue;
481
479
  }
482
480
 
@@ -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;