@applicaster/zapp-react-native-utils 15.0.0-alpha.5219062121 → 15.0.0-alpha.5394375054

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 (50) hide show
  1. package/README.md +0 -6
  2. package/actionsExecutor/ActionExecutorContext.tsx +83 -6
  3. package/appUtils/HooksManager/index.ts +35 -0
  4. package/appUtils/focusManagerAux/utils/index.ts +2 -4
  5. package/appUtils/focusManagerAux/utils/utils.ios.ts +6 -3
  6. package/appUtils/localizationsHelper.ts +4 -0
  7. package/appUtils/playerManager/index.ts +9 -0
  8. package/appUtils/playerManager/player.ts +1 -1
  9. package/appUtils/playerManager/usePlayer.tsx +5 -3
  10. package/cellUtils/__tests__/cellUtils.test.ts +39 -0
  11. package/cellUtils/index.ts +11 -1
  12. package/dateUtils/__tests__/dayjs.test.ts +0 -3
  13. package/dateUtils/index.ts +2 -0
  14. package/manifestUtils/_internals/__tests__/index.test.js +41 -0
  15. package/manifestUtils/_internals/index.js +33 -0
  16. package/manifestUtils/defaultManifestConfigurations/player.js +6 -0
  17. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +49 -0
  18. package/manifestUtils/fieldUtils/index.js +54 -0
  19. package/manifestUtils/index.js +2 -0
  20. package/manifestUtils/keys.js +228 -0
  21. package/manifestUtils/mobileAction/button/__tests__/mobileActionButton.test.js +168 -0
  22. package/manifestUtils/mobileAction/button/index.js +140 -0
  23. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +102 -0
  24. package/manifestUtils/mobileAction/container/index.js +73 -0
  25. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +127 -0
  26. package/manifestUtils/mobileAction/groups/defaults.js +76 -0
  27. package/manifestUtils/mobileAction/groups/index.js +80 -0
  28. package/numberUtils/__tests__/toNumber.test.ts +27 -12
  29. package/numberUtils/__tests__/toPositiveNumber.test.ts +32 -4
  30. package/numberUtils/index.ts +5 -1
  31. package/package.json +3 -3
  32. package/pluginUtils/index.ts +4 -5
  33. package/reactHooks/cell-click/index.ts +2 -1
  34. package/reactHooks/feed/index.ts +0 -2
  35. package/reactHooks/feed/useInflatedUrl.ts +1 -1
  36. package/reactHooks/resolvers/useComponentResolver.ts +13 -3
  37. package/reactHooks/state/__tests__/useComponentScreenState.test.ts +246 -0
  38. package/reactHooks/state/index.ts +2 -0
  39. package/reactHooks/state/useComponentScreenState.ts +45 -0
  40. package/refreshUtils/RefreshCoordinator/__tests__/refreshCoordinator.test.ts +161 -0
  41. package/refreshUtils/RefreshCoordinator/index.ts +216 -0
  42. package/refreshUtils/RefreshCoordinator/utils/__tests__/getDataRefreshConfig.test.ts +104 -0
  43. package/refreshUtils/RefreshCoordinator/utils/index.ts +29 -0
  44. package/screenPickerUtils/index.ts +5 -0
  45. package/utils/__tests__/clone.test.ts +158 -0
  46. package/utils/__tests__/path.test.ts +7 -0
  47. package/utils/clone.ts +7 -0
  48. package/utils/index.ts +2 -1
  49. package/reactHooks/feed/__tests__/useFeedRefresh.test.tsx +0 -75
  50. package/reactHooks/feed/useFeedRefresh.tsx +0 -65
package/README.md CHANGED
@@ -245,12 +245,6 @@ const connectionType = useConnectionInfo(true);
245
245
 
246
246
  `@applicaster/zapp-react-native/reactHooks`
247
247
 
248
- - `useFeedRefresh: ({ reloadData: function, component: { id: boolean | string, rules: {enable_data_refreshing: boolean, refreshing_interval: number} } }) => void` - Hook will call `reloadData` function, in the specified intervals if `enable_data_refreshing` is set to true;
249
-
250
- ```javascript
251
- useFeedRefresh({ reloadData, component });
252
- ```
253
-
254
248
  - `useFeedLoader: ({ feedUrl: string, pipesOptions?: { clearCache?: boolean, loadLocalFavourites?: boolean, silentRefresh?: boolean} }) => ({data: ?ApplicasterFeed, loading: boolean, url: string, error: Error,reloadData: (silentRefresh?: boolean) => void, loadNext: () => void})` - Hook will load data to the redux store and return a feed for the provided DSP URL. If the data for the provided url was already loaded, it will return that value
255
249
 
256
250
  ```javascript
@@ -24,6 +24,10 @@ import {
24
24
  resolveObjectValues,
25
25
  } from "../appUtils/contextKeysManager/contextResolver";
26
26
  import { useNavigation, useRivers } from "../reactHooks";
27
+ import {
28
+ getInflatedDataSourceUrl,
29
+ getSearchContext,
30
+ } from "../reactHooks/feed/useInflatedUrl";
27
31
 
28
32
  import { useContentTypes } from "@applicaster/zapp-react-native-redux/hooks";
29
33
  import { useSubscriberFor } from "../reactHooks/useSubscriberFor";
@@ -104,12 +108,42 @@ const prepareDefaultActions = (actionExecutor) => {
104
108
  async (_action, context) => {
105
109
  const dispatch = appStore.getDispatch();
106
110
 
107
- const dataSource =
108
- context?.component?.data?.source ||
109
- findParentComponent(context?.component.id, context?.screenData)?.data
110
- ?.source;
111
+ const parentComponent = findParentComponent(
112
+ context?.component?.id,
113
+ context?.screenData
114
+ );
115
+
116
+ const componentSource = context?.component?.data?.source;
111
117
 
112
- log_info(`handleAction: refreshComponent for dataSource:${dataSource}`);
118
+ const componentData = componentSource
119
+ ? context.component.data
120
+ : parentComponent?.data;
121
+
122
+ const source = componentData?.source;
123
+ const mapping = componentData?.mapping;
124
+
125
+ let dataSource = source;
126
+
127
+ if (source && mapping) {
128
+ dataSource =
129
+ getInflatedDataSourceUrl({
130
+ source,
131
+ contexts: {
132
+ entry: context?.entryContext,
133
+ screen: context?.screenData,
134
+ search: getSearchContext(null, mapping),
135
+ },
136
+ mapping,
137
+ }) || source;
138
+ }
139
+
140
+ log_info(`handleAction: refreshComponent for dataSource:${dataSource}`, {
141
+ source,
142
+ inflatedUrl: dataSource,
143
+ mapping,
144
+ entryContextId: context?.entryContext?.id,
145
+ entryId: context?.entry?.id,
146
+ });
113
147
 
114
148
  // TODO: In theory we should wait callback to complete, before completing the action, but now it's not needed
115
149
  // TODO: handle focused item removal
@@ -315,6 +349,44 @@ export function withActionExecutor(Component) {
315
349
  return ActionResult.Error;
316
350
  }
317
351
 
352
+ const navigationAction = action.options?.navigationAction;
353
+ const entrySource = action.options?.entry;
354
+
355
+ const entry = entrySource
356
+ ? entrySource === "@{entry/}"
357
+ ? context?.entry
358
+ : entrySource
359
+ : null;
360
+
361
+ if (entry) {
362
+ if (typeof entry !== "object") {
363
+ log_error(
364
+ `navigateToScreen: entry option is not an object, entry: ${entry}`
365
+ );
366
+
367
+ return ActionResult.Error;
368
+ }
369
+
370
+ log_info(
371
+ `navigateToScreen: navigating to screen type: ${screenType} with entry id: ${entry.id}`
372
+ );
373
+
374
+ const overriddenEntry = {
375
+ ...entry,
376
+ type: {
377
+ value: screenType,
378
+ },
379
+ };
380
+
381
+ if (navigationAction === "push") {
382
+ navigator.push(overriddenEntry);
383
+ } else {
384
+ navigator.replace(overriddenEntry);
385
+ }
386
+
387
+ return ActionResult.Success;
388
+ }
389
+
318
390
  const screenId = contentTypes?.[screenType]?.screen_id || null;
319
391
 
320
392
  if (!screenId) {
@@ -334,7 +406,12 @@ export function withActionExecutor(Component) {
334
406
  }
335
407
 
336
408
  context?.callback?.({ success: false, error: null, abort: true });
337
- navigator.replace(river);
409
+
410
+ if (navigationAction === "push") {
411
+ navigator.push(river);
412
+ } else {
413
+ navigator.replace(river);
414
+ }
338
415
 
339
416
  return ActionResult.Success;
340
417
  }
@@ -11,6 +11,10 @@ import { HOOKS_EVENTS, HOOKS_TYPE } from "./constants";
11
11
 
12
12
  import { hooksManagerLogger } from "./logger";
13
13
  import { HookManager, HookManagerArgs } from "./types";
14
+ import {
15
+ actionExecutor,
16
+ ActionResult,
17
+ } from "../../actionsExecutor/ActionExecutor";
14
18
 
15
19
  /**
16
20
  * orders the hooks according to their weight
@@ -280,6 +284,8 @@ export function HooksManager({
280
284
  {}
281
285
  );
282
286
 
287
+ actionExecutor.unregisterAction("finishHook");
288
+
283
289
  return;
284
290
  }
285
291
 
@@ -294,6 +300,8 @@ export function HooksManager({
294
300
  }
295
301
  );
296
302
 
303
+ actionExecutor.unregisterAction("finishHook");
304
+
297
305
  return hookPlugin.setStateAndNotify(HOOKS_EVENTS.ERROR, {
298
306
  error,
299
307
  hookPlugin,
@@ -316,6 +324,8 @@ export function HooksManager({
316
324
  // TODO: Temporary hack to pass getLoginProtocol to other plugins to refresh in case token expired, need be deleted later
317
325
  delete payload.getLoginProtocol;
318
326
 
327
+ actionExecutor.unregisterAction("finishHook");
328
+
319
329
  hookPlugin.setStateAndNotify(HOOKS_EVENTS.CANCEL, {
320
330
  hookPlugin,
321
331
  payload,
@@ -381,6 +391,27 @@ export function HooksManager({
381
391
  };
382
392
  }
383
393
 
394
+ function registerFinishHookAction(payload, callback) {
395
+ // Ensure no stale finishHook remains (e.g. presentUI re-entry from runInBackground)
396
+ actionExecutor.unregisterAction("finishHook");
397
+
398
+ actionExecutor.registerAction("finishHook", async (action: ActionType) => {
399
+ const { success, errorMessage, abort } = action.options;
400
+
401
+ actionExecutor.unregisterAction("finishHook");
402
+
403
+ if (errorMessage) {
404
+ callback({ success, error: new Error(errorMessage), payload, abort });
405
+ } else {
406
+ callback({ success, error: null, payload, abort });
407
+ }
408
+
409
+ hooksManagerLogger.info("finishHook action executed, finishing flow");
410
+
411
+ return ActionResult.Success;
412
+ });
413
+ }
414
+
384
415
  /**
385
416
  * presents a screen hook by triggering an event invoking the handler with
386
417
  * the appropriate route & payload
@@ -408,6 +439,8 @@ export function HooksManager({
408
439
  }
409
440
  );
410
441
 
442
+ registerFinishHookAction(payload, callback);
443
+
411
444
  hookPlugin.setStateAndNotify(HOOKS_EVENTS.PRESENT_SCREEN_HOOK, {
412
445
  hookPlugin,
413
446
  route: targetScreenRoute,
@@ -467,6 +500,8 @@ export function HooksManager({
467
500
  }
468
501
  );
469
502
 
503
+ registerFinishHookAction(payload, callback);
504
+
470
505
  hookPlugin.module.runInBackground(
471
506
  payload,
472
507
  callback,
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  import {
15
15
  getFocusableId,
16
- SCREEN_PICKER_CONTAINER,
16
+ isTabsScreenContentContainerId,
17
17
  } from "@applicaster/zapp-react-native-utils/screenPickerUtils";
18
18
 
19
19
  // run check each 300 ms
@@ -24,8 +24,6 @@ const isTopMenu = (node) => startsWith(QUICK_BRICK_NAVBAR, node?.id);
24
24
  const isContent = (node) => startsWith(QUICK_BRICK_CONTENT, node?.id);
25
25
  const isRoot = (node) => node?.id === "root";
26
26
 
27
- const isScrenPicker = (node) => startsWith(SCREEN_PICKER_CONTAINER, node?.id);
28
-
29
27
  type Props = {
30
28
  maxTimeout: number;
31
29
  conditionFn: () => boolean;
@@ -136,7 +134,7 @@ export const isTabsScreenOnContentFocused = (node) => {
136
134
  return false;
137
135
  }
138
136
 
139
- if (isScrenPicker(node)) {
137
+ if (isTabsScreenContentContainerId(node?.id)) {
140
138
  return true;
141
139
  }
142
140
 
@@ -106,9 +106,8 @@ const focusableNativeViewRegistration = ({ focusableView, focusableGroup }) => {
106
106
  );
107
107
  };
108
108
 
109
- export const firstFocusableViewRegistrationFactory = () =>
109
+ export const firstFocusableViewInContentRegistrationFactory = () =>
110
110
  focusableViewRegistrationSubject$.pipe(
111
- take(1), // we care about only first FocusableView registration
112
111
  switchMap((focusableView) =>
113
112
  // start waiting registration of its parent FocusableGroup
114
113
  focusableGroupRegistrationSubject$.pipe(
@@ -126,7 +125,11 @@ export const firstFocusableViewRegistrationFactory = () =>
126
125
  focusableView,
127
126
  focusableGroup,
128
127
  })
129
- )
128
+ ),
129
+ filter(({ focusableView }) =>
130
+ isPartOfContent(focusManager.focusableTree, focusableView.id)
131
+ ),
132
+ take(1) // we care about only first FocusableView registration
130
133
  );
131
134
 
132
135
  // registration on RN level(into RN focusManager)
@@ -85,6 +85,10 @@ export const toDayJSLocaleMap = (code) => {
85
85
  tt: null,
86
86
  "es-LA": "es",
87
87
  "en-UK": "en-gb",
88
+ "en-ZA": null,
89
+ "en-NG": null,
90
+ "en-ZW": null,
91
+ "en-ZM": null,
88
92
  };
89
93
 
90
94
  if (map[code] === null) {
@@ -33,6 +33,7 @@ function deprecationWarning(method) {
33
33
  export interface PlayerLifecycleListener {
34
34
  onRegistered?: (player: Player) => void;
35
35
  onUnRegistered?: (player: Player) => void;
36
+ onCastingChanged?: (isCasting: boolean) => void;
36
37
  }
37
38
 
38
39
  type PlayersMap = {
@@ -253,10 +254,18 @@ export class PlayerManager {
253
254
 
254
255
  public registerCastingReceiver(receiver: Player) {
255
256
  this.castingReceiver = receiver;
257
+
258
+ this.lifecycleListeners.forEach((listener) => {
259
+ listener.onCastingChanged?.(true);
260
+ });
256
261
  }
257
262
 
258
263
  public unregisterCastingReceiver() {
259
264
  this.castingReceiver = null;
265
+
266
+ this.lifecycleListeners.forEach((listener) => {
267
+ listener.onCastingChanged?.(false);
268
+ });
260
269
  }
261
270
 
262
271
  /**
@@ -258,7 +258,7 @@ export class Player {
258
258
  };
259
259
 
260
260
  isAd = () => {
261
- return !!this.playerState.adState || this.playerState.isInAdBreak;
261
+ return !!this.playerState.adState || !!this.playerState.isInAdBreak;
262
262
  };
263
263
 
264
264
  isSeeking = () => {
@@ -22,7 +22,6 @@ export const usePlayer = (playerId?: string): Player => {
22
22
 
23
23
  const isCasting = playerManager.isCasting();
24
24
 
25
- // TODO: We need to prerender when we start/stop casting, fix this
26
25
  const getPlayer = (playerId, isCasting) =>
27
26
  getControlledPlayer(playerId, isCasting);
28
27
 
@@ -46,7 +45,7 @@ export const usePlayer = (playerId?: string): Player => {
46
45
  return playerManager.addLifecycleListener({
47
46
  onRegistered: (player) => {
48
47
  if (playerIdToUse === player.playerId) {
49
- setPlayer(getPlayer(playerIdToUse, isCasting));
48
+ setPlayer(getPlayer(playerIdToUse, playerManager.isCasting()));
50
49
  }
51
50
  },
52
51
  onUnRegistered: (player) => {
@@ -54,8 +53,11 @@ export const usePlayer = (playerId?: string): Player => {
54
53
  setPlayer(null);
55
54
  }
56
55
  },
56
+ onCastingChanged: () => {
57
+ setPlayer(getPlayer(playerIdToUse, playerManager.isCasting()));
58
+ },
57
59
  } as PlayerLifecycleListener);
58
- }, [playerIdToUse, isCasting]);
60
+ }, [playerIdToUse]);
59
61
 
60
62
  return player;
61
63
  };
@@ -3,6 +3,7 @@ import {
3
3
  ifEmptyUseFallback,
4
4
  textTransform,
5
5
  getKeyForLabel,
6
+ getDataTransformForLabel,
6
7
  getLabel,
7
8
  getAspectRatio,
8
9
  getCellWidth,
@@ -56,6 +57,44 @@ describe("getKeyForLabel", () => {
56
57
  });
57
58
  });
58
59
 
60
+ describe("getDataTransformForLabel", () => {
61
+ it("returns dataKey when dataKey is not other", () => {
62
+ expect(getDataTransformForLabel(VAL.dataKey, VAL.customKey)).toEqual(
63
+ VAL.dataKey
64
+ );
65
+ });
66
+
67
+ it("returns customKey when dataKey is other", () => {
68
+ expect(getDataTransformForLabel(VAL.other, VAL.customKey)).toEqual(
69
+ VAL.customKey
70
+ );
71
+ });
72
+
73
+ it("preserves full date formats containing dots", () => {
74
+ expect(getDataTransformForLabel(VAL.other, "DD.MM.YYYY")).toEqual(
75
+ "DD.MM.YYYY"
76
+ );
77
+ });
78
+
79
+ it("returns default date format when selected key is null or undefined", () => {
80
+ expect(getDataTransformForLabel(EMPTY.nulls, VAL.customKey)).toEqual(
81
+ "DD/MM/YYYY"
82
+ );
83
+
84
+ expect(getDataTransformForLabel(EMPTY.undef, VAL.customKey)).toEqual(
85
+ "DD/MM/YYYY"
86
+ );
87
+
88
+ expect(getDataTransformForLabel(VAL.other, EMPTY.nulls)).toEqual(
89
+ "DD/MM/YYYY"
90
+ );
91
+
92
+ expect(getDataTransformForLabel(VAL.other, EMPTY.undef)).toEqual(
93
+ "DD/MM/YYYY"
94
+ );
95
+ });
96
+ });
97
+
59
98
  describe("getLabel", () => {
60
99
  it("returns a string from dataKey's path", () => {
61
100
  expect(getLabel(VAL.dataKey, VAL.customKey)(entry)).toEqual(entry.id);
@@ -1,5 +1,5 @@
1
1
  import * as R from "ramda";
2
- import dayjs from "dayjs";
2
+ import { dayjs } from "../dateUtils";
3
3
  import validateColor from "validate-color";
4
4
 
5
5
  import { transformColorCode as fixColorHexCode } from "@applicaster/zapp-react-native-utils/transform";
@@ -31,6 +31,16 @@ export const getKeyForLabel = (dataKey, customKey) => {
31
31
  return keyToUse.split(".");
32
32
  };
33
33
 
34
+ /**
35
+ * Provides `format` for date formatting. Returns customKey if dataKey is "other", otherwise it returns dataKey.
36
+ * Should consist of valid dayjs tokens - https://day.js.org/docs/en/parse/string-format#list-of-all-available-parsing-tokens
37
+ */
38
+ export const getDataTransformForLabel = (dataKey, customKey) => {
39
+ const keyToUse = dataKey === CUSTOM_KEY ? customKey : dataKey;
40
+
41
+ return keyToUse ?? "DD/MM/YYYY";
42
+ };
43
+
34
44
  /**
35
45
  * This method will return true if the argument passed to it is either empty or nil
36
46
  * The method prevents zero from being evaluated as falsey in an || condition
@@ -1,7 +1,4 @@
1
1
  import { dayjs } from "../index";
2
- import customParseFormat from "dayjs/plugin/customParseFormat";
3
-
4
- dayjs.extend(customParseFormat);
5
2
 
6
3
  describe("dayjs", () => {
7
4
  describe("basic functionality", () => {
@@ -4,12 +4,14 @@ import timezone from "dayjs/plugin/timezone";
4
4
  import isoWeek from "dayjs/plugin/isoWeek";
5
5
  import duration from "dayjs/plugin/duration";
6
6
  import relativeTime from "dayjs/plugin/relativeTime";
7
+ import customParseFormat from "dayjs/plugin/customParseFormat";
7
8
 
8
9
  // Extend dayjs with plugins
9
10
  dayjs.extend(utc);
10
11
  dayjs.extend(timezone);
11
12
  dayjs.extend(isoWeek);
12
13
  dayjs.extend(duration);
14
+ dayjs.extend(customParseFormat);
13
15
 
14
16
  dayjs.extend(relativeTime, {
15
17
  thresholds: [
@@ -0,0 +1,41 @@
1
+ const {
2
+ isKeyHasSuffix,
3
+ isKeyHasAnyOfSuffixes,
4
+ getKeyWithPrefixGenerator,
5
+ } = require("..");
6
+
7
+ describe("manifestUtils/_internals helpers", () => {
8
+ it("checks a key suffix", () => {
9
+ expect(
10
+ isKeyHasSuffix("button_enabled", "mobile_button_1_button_enabled")
11
+ ).toBe(true);
12
+
13
+ expect(
14
+ isKeyHasSuffix("button_enabled", "mobile_button_1_assign_action")
15
+ ).toBe(false);
16
+ });
17
+
18
+ it("checks key against multiple suffixes", () => {
19
+ const suffixes = ["font_color", "focused_font_color", "text_transform"];
20
+
21
+ expect(
22
+ isKeyHasAnyOfSuffixes(suffixes, "mobile_button_1_text_transform")
23
+ ).toBe(true);
24
+
25
+ expect(
26
+ isKeyHasAnyOfSuffixes(suffixes, "mobile_button_1_asset_alignment")
27
+ ).toBe(false);
28
+ });
29
+
30
+ it("generates stable key names using a prefix", () => {
31
+ const withMobileButtonPrefix = getKeyWithPrefixGenerator("mobile_button_2");
32
+
33
+ expect(withMobileButtonPrefix("display_mode")).toBe(
34
+ "mobile_button_2_display_mode"
35
+ );
36
+
37
+ expect(withMobileButtonPrefix("asset_enabled")).toBe(
38
+ "mobile_button_2_asset_enabled"
39
+ );
40
+ });
41
+ });
@@ -174,6 +174,36 @@ function generateFieldsFromDefaultsWithoutPrefixedLabel(
174
174
  )(fields);
175
175
  }
176
176
 
177
+ /**
178
+ * Checks whether a generated manifest field key ends with the given suffix.
179
+ *
180
+ * @param {string} suffix
181
+ * @param {string} key
182
+ * @returns {boolean}
183
+ */
184
+ const isKeyHasSuffix = (suffix, key) => key.endsWith(`_${suffix}`);
185
+
186
+ /**
187
+ * Checks whether a generated manifest field key ends with any supported suffix.
188
+ *
189
+ * @param {string[]} suffixes
190
+ * @param {string} key
191
+ * @returns {boolean}
192
+ */
193
+ const isKeyHasAnyOfSuffixes = (suffixes, key) =>
194
+ suffixes.some((suffix) => key.endsWith(`_${suffix}`));
195
+
196
+ /**
197
+ * Creates a helper that prefixes a manifest field suffix with a shared key stem.
198
+ *
199
+ * @param {string} prefix
200
+ * @returns {(suffix: string) => string}
201
+ */
202
+ function getKeyWithPrefixGenerator(prefix) {
203
+ // expect prefix as lower snake case, e.g. "mobile_buttons_container"
204
+ return (suffix) => `${prefix}_${suffix}`;
205
+ }
206
+
177
207
  module.exports = {
178
208
  toSnakeCase,
179
209
  toCamelCase,
@@ -185,4 +215,7 @@ module.exports = {
185
215
  getDefaultConfiguration,
186
216
  compact,
187
217
  replaceUnderscoreToSpace,
218
+ isKeyHasSuffix,
219
+ isKeyHasAnyOfSuffixes,
220
+ getKeyWithPrefixGenerator,
188
221
  };
@@ -123,6 +123,12 @@ function getPlayerConfiguration({ platform, version }) {
123
123
  key: "skip_button_localization_text_skip_intro",
124
124
  initial_value: "Skip Intro",
125
125
  },
126
+ {
127
+ type: "text_input",
128
+ label: "Stream error message",
129
+ key: "stream_error_message",
130
+ initial_value: "Cannot play stream",
131
+ },
126
132
  {
127
133
  type: "text_input",
128
134
  label: "Locked message",
@@ -0,0 +1,49 @@
1
+ const {
2
+ withConditional,
3
+ getConditionalKey,
4
+ createConditionalField,
5
+ } = require("..");
6
+
7
+ describe("manifestUtils/fieldUtils", () => {
8
+ it("appends conditions and adds all_conditions when there is more than one condition", () => {
9
+ const config = {
10
+ key: "mobile_button_1_width",
11
+ conditional_fields: [
12
+ { key: "styles/mobile_button_1_button_enabled", condition_value: true },
13
+ ],
14
+ };
15
+
16
+ const result = withConditional([
17
+ { key: "styles/mobile_button_1_display_mode", condition_value: "fixed" },
18
+ ])(config);
19
+
20
+ expect(result.conditional_fields).toEqual([
21
+ { key: "styles/mobile_button_1_button_enabled", condition_value: true },
22
+ { key: "styles/mobile_button_1_display_mode", condition_value: "fixed" },
23
+ ]);
24
+
25
+ expect(result.rules).toBe("all_conditions");
26
+ });
27
+
28
+ it("creates category-prefixed conditional key", () => {
29
+ expect(
30
+ getConditionalKey("mobile_buttons_container_position", "styles")
31
+ ).toBe("styles/mobile_buttons_container_position");
32
+ });
33
+
34
+ it("creates conditional fields with default category and with raw key", () => {
35
+ expect(
36
+ createConditionalField("mobile_button_1_asset_enabled", true)
37
+ ).toEqual({
38
+ key: "styles/mobile_button_1_asset_enabled",
39
+ condition_value: true,
40
+ });
41
+
42
+ expect(
43
+ createConditionalField("mobile_button_1_asset_enabled", true, null)
44
+ ).toEqual({
45
+ key: "mobile_button_1_asset_enabled",
46
+ condition_value: true,
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Appends new conditional_fields from conditions array
3
+ * to config.conditional_fields
4
+ * if there are more than 1 condition, sets rules to "all_conditions"
5
+ *
6
+ * @param {Array<{key: string, condition_value: unknown}>} conditions
7
+ * @returns {(config: object) => object}
8
+ */
9
+ const withConditional = (conditions) => (config) => {
10
+ const conditional_fields = [
11
+ ...(config.conditional_fields || []),
12
+ ...conditions,
13
+ ];
14
+
15
+ const next = { ...config, conditional_fields };
16
+
17
+ if (conditional_fields.length > 1) {
18
+ return { ...next, rules: "all_conditions" };
19
+ }
20
+
21
+ return next;
22
+ };
23
+
24
+ /**
25
+ * Builds key for conditional fields prepending category, e.g. "styles/mobile_buttons_container_position"
26
+ *
27
+ * @param {string} key
28
+ * @param {string} category
29
+ * @returns {string}
30
+ */
31
+ function getConditionalKey(key, category) {
32
+ return `${category}/${key}`;
33
+ }
34
+
35
+ /**
36
+ * Returns a conditional field object for manifest visibility rules.
37
+ *
38
+ * @param {string} key
39
+ * @param {unknown} condition_value
40
+ * @param {string|null} [category="styles"] Pass `null` to use the key as-is.
41
+ * @returns {{key: string, condition_value: unknown}}
42
+ */
43
+ function createConditionalField(key, condition_value, category = "styles") {
44
+ return {
45
+ key: category ? getConditionalKey(key, category) : key,
46
+ condition_value,
47
+ };
48
+ }
49
+
50
+ module.exports = {
51
+ withConditional,
52
+ getConditionalKey,
53
+ createConditionalField,
54
+ };
@@ -8,6 +8,7 @@ const {
8
8
 
9
9
  const { tvActionButtonsContainer } = require("./tvAction/container");
10
10
  const { tvActionButton } = require("./tvAction/button");
11
+ const { buildMobileActionButtonGroups } = require("./mobileAction/groups");
11
12
  const { compact } = require("./_internals");
12
13
 
13
14
  const { spacingKey, absolutePositionElement } = require("./containers");
@@ -43,6 +44,7 @@ module.exports = {
43
44
  tvMenuLabel,
44
45
  tvActionButtonsContainer,
45
46
  tvActionButton,
47
+ buildMobileActionButtonGroups,
46
48
  mobileCellLabel,
47
49
  tvCellLabel,
48
50
  tvBadges,