@applicaster/zapp-react-native-utils 12.0.0-alpha.5812543239 → 12.0.0-alpha.5895113034

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 (93) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +16 -8
  2. package/analyticsUtils/AnalyticPlayerListener.ts +155 -0
  3. package/analyticsUtils/AnalyticsEvents/index.ts +6 -0
  4. package/analyticsUtils/AnalyticsEvents/searchEvents.ts +21 -0
  5. package/analyticsUtils/PlayerAnalyticsManager.ts +90 -0
  6. package/analyticsUtils/__tests__/analyticsManager.test.js +75 -73
  7. package/analyticsUtils/analyticsPlugins.ts +25 -0
  8. package/analyticsUtils/events.ts +6 -0
  9. package/analyticsUtils/helpers/hooks.ts +1 -1
  10. package/analyticsUtils/index.tsx +6 -0
  11. package/analyticsUtils/{analyticsTracker.ts → playerAnalyticsTracker.ts} +19 -10
  12. package/appUtils/HooksManager/index.js +3 -1
  13. package/appUtils/focusManager/treeDataStructure/Utils/index.js +12 -3
  14. package/appUtils/keyCodes/__tests__/__snapshots__/keyCodes.test.js.snap +1 -1
  15. package/appUtils/keyCodes/__tests__/keyCodes.test.js +80 -85
  16. package/appUtils/keyCodes/keys.web.js +1 -0
  17. package/appUtils/orientationHelper.ts +1 -1
  18. package/appUtils/playerManager/DelayedSeekPlayerWrapper.tsx +179 -0
  19. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +53 -0
  20. package/appUtils/playerManager/conts.ts +5 -0
  21. package/appUtils/playerManager/index.ts +9 -27
  22. package/appUtils/playerManager/player.ts +60 -9
  23. package/appUtils/playerManager/playerFactory.ts +15 -1
  24. package/appUtils/playerManager/playerNative.ts +39 -3
  25. package/appUtils/playerManager/usePlayerControllerSetup.tsx +23 -4
  26. package/appUtils/playerManager/usePlayerState.tsx +13 -1
  27. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +50 -0
  28. package/cellUtils/__tests__/cellUtils.test.ts +48 -0
  29. package/cellUtils/__tests__/fixtures.ts +13 -0
  30. package/cellUtils/index.ts +40 -2
  31. package/dateUtils/index.ts +11 -0
  32. package/focusManager/index.ts +1 -1
  33. package/functionUtils/__tests__/__snapshots__/functionUtils.test.js.snap +2 -2
  34. package/functionUtils/__tests__/functionUtils.test.js +244 -248
  35. package/headersUtils/index.ts +1 -0
  36. package/inflatedUrlUtils/index.ts +4 -4
  37. package/localizationUtils/__tests__/localizationUtils.test.ts +78 -55
  38. package/localizationUtils/index.ts +13 -19
  39. package/logger/index.ts +1 -1
  40. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  41. package/manifestUtils/defaultManifestConfigurations/player.js +10 -1
  42. package/manifestUtils/index.js +5 -1
  43. package/manifestUtils/secondaryImage/index.js +18 -15
  44. package/modalState/index.ts +57 -0
  45. package/navigationUtils/index.ts +56 -7
  46. package/numberUtils/index.ts +4 -0
  47. package/package.json +3 -2
  48. package/playerUtils/configurationGenerator.ts +11 -1
  49. package/pluginUtils/index.ts +11 -7
  50. package/reactHooks/autoscrolling/__tests__/useTrackCurrentAutoScrollingElement.test.ts +2 -0
  51. package/reactHooks/autoscrolling/__tests__/{useTrackedView.test.ts → useTrackedView.test.tsx} +18 -1
  52. package/reactHooks/cell-click/__tests__/index.test.js +11 -2
  53. package/reactHooks/cell-click/index.ts +4 -4
  54. package/reactHooks/device/index.ts +0 -1
  55. package/reactHooks/device/useIsTablet.tsx +28 -3
  56. package/reactHooks/device/useMemoizedIsTablet.ts +1 -1
  57. package/reactHooks/device/useOrientation.tsx +3 -1
  58. package/reactHooks/feed/__tests__/useInflatedUrl.test.ts +18 -3
  59. package/reactHooks/feed/useInflatedUrl.ts +44 -15
  60. package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +16 -9
  61. package/reactHooks/index.ts +2 -0
  62. package/reactHooks/layout/__tests__/{index.test.js → index.test.tsx} +66 -59
  63. package/reactHooks/layout/getDeviceInfo/index.ts +4 -2
  64. package/reactHooks/layout/index.ts +2 -1
  65. package/reactHooks/layout/types/index.ts +2 -0
  66. package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.native.test.js +11 -16
  67. package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.test.js +12 -14
  68. package/reactHooks/layout/useDimensions/useDimensions.ts +13 -7
  69. package/reactHooks/navigation/__mocks__/index.ts +15 -0
  70. package/reactHooks/navigation/__mocks__/useNavigation.ts +7 -0
  71. package/reactHooks/navigation/__tests__/index.test.tsx +240 -81
  72. package/reactHooks/navigation/index.ts +208 -86
  73. package/reactHooks/player/__tests__/useAutoSeek.test.tsx +8 -1
  74. package/reactHooks/player/useAutoSeek.tsx +28 -8
  75. package/reactHooks/player/useSeek.tsx +4 -2
  76. package/reactHooks/player/useTapSeek.tsx +34 -7
  77. package/reactHooks/resolvers/__tests__/useCellResolver.test.tsx +3 -1
  78. package/reactHooks/screen/__mocks__/useScreenContext.ts +3 -1
  79. package/reactHooks/screen/index.ts +2 -0
  80. package/reactHooks/screen/useScreenContext.ts +5 -4
  81. package/reactHooks/screen/useScreenData.ts +1 -1
  82. package/reactHooks/screen/useTargetScreenData.ts +7 -0
  83. package/reactHooks/state/useRivers.ts +3 -1
  84. package/reactHooks/useListenEventBusEvent.ts +1 -4
  85. package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +10 -9
  86. package/riverComponetsMeasurementProvider/index.tsx +2 -2
  87. package/stringUtils/index.ts +5 -0
  88. package/testUtils/getByTestId.ts +14 -0
  89. package/testUtils/index.tsx +38 -23
  90. package/types.ts +4 -0
  91. package/reactHooks/device/useIsTablet.android.tsx +0 -14
  92. package/reactHooks/device/useIsTablet.ios.tsx +0 -7
  93. package/reactHooks/device/useIsTablet.web.tsx +0 -3
@@ -42,7 +42,7 @@ export class Player {
42
42
 
43
43
  protected startPosition?: number | null;
44
44
  readonly playerRole: PlayerRole = PlayerRole.Unspecified;
45
- private chaptersObserver?: OverlaysObserver | null;
45
+ private overlaysObserver?: OverlaysObserver | null;
46
46
  private entryObservable?: BehaviorSubject<ZappEntry> | null;
47
47
 
48
48
  protected listeners: {
@@ -91,15 +91,17 @@ export class Player {
91
91
  this.onVideoProgress = this.onVideoProgress.bind(this);
92
92
  this.onVideoLoad = this.onVideoLoad.bind(this);
93
93
  this.isFullScreenSupported = this.isFullScreenSupported.bind(this);
94
+ this.onPlayerSeekComplete = this.onPlayerSeekComplete.bind(this);
95
+
94
96
  this.entryObservable = null;
95
97
  }
96
98
 
97
99
  getOverlayObservable = () => {
98
- if (!this.chaptersObserver) {
99
- this.chaptersObserver = new OverlaysObserver({ player: this });
100
+ if (!this.overlaysObserver) {
101
+ this.overlaysObserver = new OverlaysObserver({ player: this });
100
102
  }
101
103
 
102
- return this.chaptersObserver;
104
+ return this.overlaysObserver;
103
105
  };
104
106
 
105
107
  getEntryObservable = (): BehaviorSubject<ZappEntry | null> => {
@@ -278,6 +280,10 @@ export class Player {
278
280
  return this.playerState.isBuffering;
279
281
  };
280
282
 
283
+ isReadyToPlay = (): boolean => {
284
+ return this.playerState.isReadyToPlay;
285
+ };
286
+
281
287
  // Methods to call actions on the player
282
288
  seekTo = (_position: number) => {};
283
289
 
@@ -317,7 +323,7 @@ export class Player {
317
323
  * You should use dispatchPlayerEvent at PlayerContainer or call playerManager.invokeHandler directly
318
324
  * This allows you to use one method for everything.
319
325
  */
320
- onSeekStart = (event: QuickBrickPlayer.OnSeekStartEvent) => {
326
+ onPlayerSeekStart = (event: QuickBrickPlayer.OnSeekStartEvent) => {
321
327
  const { isLive, seekableDuration, currentTime, duration, seekTime } = event;
322
328
 
323
329
  this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });
@@ -332,10 +338,11 @@ export class Player {
332
338
  });
333
339
  };
334
340
 
335
- onSeekComplete = (event: QuickBrickPlayer.OnSeekCompleteEvent) => {
341
+ onPlayerSeekComplete(event: QuickBrickPlayer.OnSeekCompleteEvent) {
336
342
  const { isLive, seekableDuration, currentTime, duration } = event;
337
343
 
338
344
  this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });
345
+
339
346
  this.playerState.seekPosition = null;
340
347
 
341
348
  if (!isLive) {
@@ -346,7 +353,7 @@ export class Player {
346
353
  callbackName: SharedPlayerCallBacksKeys.OnPlayerSeekComplete,
347
354
  event,
348
355
  });
349
- };
356
+ }
350
357
 
351
358
  onVideoEnd = (event) => {
352
359
  this.invokeListenersUpdate({
@@ -410,6 +417,32 @@ export class Player {
410
417
  });
411
418
  };
412
419
 
420
+ onVideoFullscreenPlayerWillDismiss = () => {
421
+ this.invokeListenersUpdate({
422
+ callbackName:
423
+ SharedPlayerCallBacksKeys.onVideoFullscreenPlayerWillDismiss,
424
+ });
425
+ };
426
+
427
+ onVideoFullscreenPlayerDidDismiss = () => {
428
+ this.invokeListenersUpdate({
429
+ callbackName: SharedPlayerCallBacksKeys.onVideoFullscreenPlayerDidDismiss,
430
+ });
431
+ };
432
+
433
+ onVideoFullscreenPlayerWillPresent = () => {
434
+ this.invokeListenersUpdate({
435
+ callbackName:
436
+ SharedPlayerCallBacksKeys.onVideoFullscreenPlayerWillPresent,
437
+ });
438
+ };
439
+
440
+ onVideoFullscreenPlayerDidPresent = () => {
441
+ this.invokeListenersUpdate({
442
+ callbackName: SharedPlayerCallBacksKeys.onVideoFullscreenPlayerDidPresent,
443
+ });
444
+ };
445
+
413
446
  public invokeListenersUpdate = ({
414
447
  callbackName,
415
448
  event = {},
@@ -616,8 +649,8 @@ export class Player {
616
649
  };
617
650
 
618
651
  public getListener = (): QuickBrickPlayer.SharedPlayerCallBacks => ({
619
- onPlayerSeekStart: this.onSeekStart,
620
- onPlayerSeekComplete: this.onSeekComplete,
652
+ onPlayerSeekStart: this.onPlayerSeekStart,
653
+ onPlayerSeekComplete: this.onPlayerSeekComplete,
621
654
  onVideoEnd: this.onVideoEnd,
622
655
  onError: this.onError,
623
656
  onLoad: this.onLoad,
@@ -630,6 +663,12 @@ export class Player {
630
663
  onBufferStart: this.onBufferStart,
631
664
  onBufferComplete: this.onBufferComplete,
632
665
 
666
+ onPlayerClose: this.onPlayerClose,
667
+ onVideoFullscreenPlayerWillPresent: this.onVideoFullscreenPlayerWillPresent,
668
+ onVideoFullscreenPlayerDidPresent: this.onVideoFullscreenPlayerDidPresent,
669
+ onVideoFullscreenPlayerWillDismiss: this.onVideoFullscreenPlayerWillDismiss,
670
+ onVideoFullscreenPlayerDidDismiss: this.onVideoFullscreenPlayerDidDismiss,
671
+
633
672
  // Ads Callbacks:
634
673
  onAdBegin: this.onAdBegin,
635
674
  onAdBreakBegin: this.onAdBreakBegin,
@@ -652,6 +691,10 @@ export class Player {
652
691
  _selected?: QuickBrickPlayer.TextTrack | QuickBrickPlayer.AudioTrack
653
692
  ) => {};
654
693
 
694
+ setPlaybackRate = (_rate) => {};
695
+ startSleepTimer = (_timestamp) => {};
696
+ cancelSleepTimer = () => {};
697
+
655
698
  getPluginConfiguration = () => null;
656
699
  appStateChange = (_appState, _previousAppState) => {};
657
700
 
@@ -685,4 +728,12 @@ export class Player {
685
728
  isFullScreenSupported(): boolean {
686
729
  return true;
687
730
  }
731
+
732
+ public supportsNativeControls = (): boolean => false;
733
+ public supportNativeCast = (): boolean => false;
734
+
735
+ public isInDelayedSeek = (): boolean => false;
736
+ public enterDelayedSeek() {}
737
+
738
+ public exitDelayedSeek() {}
688
739
  }
@@ -1,17 +1,31 @@
1
1
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
2
2
  import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
3
3
  import { Player } from "./player";
4
+ import { PlayerRole } from "./conts";
4
5
 
5
6
  type PlayerFactoryItem = {
6
7
  Component: any;
7
8
  controller: Player;
8
9
  };
9
10
 
11
+ type PlayerFactoryProps = {
12
+ player: any;
13
+ playerId: string;
14
+ autoplay: boolean;
15
+ entry: ZappEntry;
16
+ muted: boolean;
17
+ playerPluginId: string;
18
+ screenConfig: Record<string, any>;
19
+ playerRole: PlayerRole;
20
+ };
21
+
10
22
  interface PlayerConstructor {
11
23
  new (params: any): Player;
12
24
  }
13
25
 
14
- export const playerFactory = (config: any): PlayerFactoryItem | null => {
26
+ export const playerFactory = (
27
+ config: PlayerFactoryProps
28
+ ): PlayerFactoryItem | null => {
15
29
  const IDENTIFIER = config?.playerPluginId;
16
30
 
17
31
  if (!IDENTIFIER) {
@@ -5,6 +5,7 @@ import { Player } from "./player";
5
5
  import { findNodeHandle, NativeModules } from "react-native";
6
6
  import { isApplePlatform } from "../../reactUtils";
7
7
  import { PlayerRole } from "./conts";
8
+ import { toBooleanWithDefaultFalse } from "../../booleanUtils";
8
9
 
9
10
  const RNPlayerManager = {
10
11
  QuickBrickPlayerPlugin: isApplePlatform()
@@ -41,6 +42,9 @@ const PlayerModuleFuncNames = {
41
42
  rewind: "rewind",
42
43
  play: "play",
43
44
  pause: "pause",
45
+ startSleepTimer: "startSleepTimer",
46
+ cancelSleepTimer: "cancelSleepTimer",
47
+ getSleepTimerEnd: "getSleepTimerEnd",
44
48
  };
45
49
 
46
50
  export class PlayerNative extends Player {
@@ -66,7 +70,7 @@ export class PlayerNative extends Player {
66
70
  currentPlayerComponent = (): ReactComponent<PlayerProps> =>
67
71
  this.playerComponent?.current;
68
72
 
69
- getState = () => this.currentPlayerComponent()?.["getState"]?.() ?? {};
73
+ getState = () => this.playerState;
70
74
 
71
75
  // TODO: Try to add module interface
72
76
  invokeNativeFunction = (funcName: string, ...params: any[]): boolean => {
@@ -107,7 +111,7 @@ export class PlayerNative extends Player {
107
111
  return;
108
112
  }
109
113
 
110
- const newPosition = R.clamp(0, position, this.playerState.seekableDuration);
114
+ const newPosition = R.clamp(0, this.playerState.seekableDuration, position);
111
115
 
112
116
  if (
113
117
  !this.invokeNativeFunction(
@@ -172,6 +176,25 @@ export class PlayerNative extends Player {
172
176
  }
173
177
  };
174
178
 
179
+ startSleepTimer = (sleepTimestamp: number) => {
180
+ if (
181
+ !this.invokeNativeFunction(
182
+ PlayerModuleFuncNames.startSleepTimer,
183
+ sleepTimestamp
184
+ )
185
+ ) {
186
+ this.currentPlayerComponent()?.["startSleepTimer"](sleepTimestamp);
187
+ }
188
+ };
189
+
190
+ cancelSleepTimer = () => {
191
+ if (!this.invokeNativeFunction(PlayerModuleFuncNames.cancelSleepTimer)) {
192
+ this.currentPlayerComponent()?.[
193
+ PlayerModuleFuncNames.cancelSleepTimer
194
+ ]?.();
195
+ }
196
+ };
197
+
175
198
  disableBufferAnimation = (): boolean => false;
176
199
 
177
200
  setLanguage = (
@@ -180,6 +203,10 @@ export class PlayerNative extends Player {
180
203
  this.currentPlayerComponent()?.["setLanguage"]?.(selected);
181
204
  };
182
205
 
206
+ setPlaybackRate = (rate: number) => {
207
+ this.currentPlayerComponent()?.["setPlaybackRate"]?.(rate);
208
+ };
209
+
183
210
  getPluginConfiguration = () => {
184
211
  return (this.currentPlayerComponent() as any)?.props?.pluginConfiguration;
185
212
  };
@@ -204,9 +231,10 @@ export class PlayerNative extends Player {
204
231
 
205
232
  onVideoLoad = (event) => {
206
233
  if (this.startPosition) {
207
- super.onVideoLoad({ ...event, ignoreContinueWatching: true });
208
234
  this.seekTo(this.startPosition);
209
235
  this.startPosition = null;
236
+
237
+ super.onVideoLoad({ ...event, ignoreContinueWatching: true });
210
238
  } else {
211
239
  super.onVideoLoad(event);
212
240
  }
@@ -265,4 +293,12 @@ export class PlayerNative extends Player {
265
293
 
266
294
  return !(isFullScreenAudioPlayer && this.isAudioItem());
267
295
  };
296
+
297
+ supportsNativeControls = (): boolean =>
298
+ toBooleanWithDefaultFalse(this.getConfig()?.["supports_native_controls"]);
299
+
300
+ supportNativeCast = (): boolean =>
301
+ toBooleanWithDefaultFalse(
302
+ this.playerPluginId === "quick-brick-player-brightcove"
303
+ );
268
304
  }
@@ -4,6 +4,9 @@ import { useEffect } from "react";
4
4
  import { playerManager } from "./index";
5
5
  import { useValidatePlayerConfig } from "../../playerUtils/useValidatePlayerConfig";
6
6
  import { PlayerRole } from "./conts";
7
+ import { isAppleTV } from "@applicaster/zapp-react-native-ui-components/Helpers/Platform";
8
+ import { isTV } from "../../reactUtils";
9
+ import { DelayedSeekPlayerWrapper } from "./DelayedSeekPlayerWrapper";
7
10
 
8
11
  // TODO: Rename to ControllerType
9
12
  export const usePlayerControllerSetup = ({
@@ -34,17 +37,33 @@ export const usePlayerControllerSetup = ({
34
37
  useValidatePlayerConfig(config);
35
38
 
36
39
  useEffect(() => {
37
- const newPlayerController = new ControllerType({
40
+ const newConfig = isAppleTV()
41
+ ? { supports_native_controls: true, ...config }
42
+ : config;
43
+
44
+ const combinedProps = {
38
45
  player: ref,
39
46
  listener: props as any,
40
- config,
47
+ config: newConfig,
41
48
  playerId,
42
49
  entry,
43
50
  playerPluginId: pluginId,
44
51
  playerRole: PlayerRole.Primary,
45
- });
52
+ };
46
53
 
47
- setPlayerController(newPlayerController);
54
+ if (isTV()) {
55
+ const wrappedPlayerController = new ControllerType({
56
+ ...combinedProps,
57
+ playerRole: PlayerRole.Unspecified,
58
+ playerId: playerId + "." + "wrapped",
59
+ });
60
+
61
+ setPlayerController(
62
+ new DelayedSeekPlayerWrapper(combinedProps, wrappedPlayerController)
63
+ );
64
+ } else {
65
+ setPlayerController(new ControllerType(combinedProps));
66
+ }
48
67
  }, [playerId, entry?.id]);
49
68
 
50
69
  React.useEffect(() => {
@@ -9,6 +9,7 @@ type PlayerState = {
9
9
  isLive: boolean;
10
10
  isPaused: boolean;
11
11
  isBuffering: boolean;
12
+ isReadyToPlay: boolean;
12
13
  };
13
14
 
14
15
  export const usePlayerState = (
@@ -20,12 +21,14 @@ export const usePlayerState = (
20
21
  const [seekableDuration, setSeekableDuration] = React.useState<number>(null);
21
22
  const [isLive, setIsLive] = React.useState<boolean>(null);
22
23
  const [isBuffering, setIsBuffering] = React.useState<boolean>(false);
24
+ const [isReadyToPlay, setIsReadyToPlay] = React.useState<boolean>(false);
23
25
  const [isPaused, setIsPaused] = React.useState<boolean>(false);
24
26
 
25
27
  const player: Player = usePlayer(playerId);
26
28
 
27
29
  const onPlayerChangeState = React.useCallback(() => {
28
30
  setIsBuffering(player.isBuffering());
31
+ setIsReadyToPlay(player.isReadyToPlay);
29
32
  setCurrentTime(player.getContentPosition());
30
33
  setDuration(player.getDuration());
31
34
  setSeekableDuration(player.getSeekableDuration());
@@ -60,7 +63,16 @@ export const usePlayerState = (
60
63
  isLive,
61
64
  isPaused,
62
65
  isBuffering,
66
+ isReadyToPlay,
63
67
  }),
64
- [currentTime, duration, seekableDuration, isLive, isPaused, isBuffering]
68
+ [
69
+ currentTime,
70
+ duration,
71
+ seekableDuration,
72
+ isLive,
73
+ isPaused,
74
+ isBuffering,
75
+ isReadyToPlay,
76
+ ]
65
77
  );
66
78
  };
@@ -0,0 +1,50 @@
1
+ import { useEffect, useState } from "react";
2
+ import { OverlaysObserver } from "./OverlayObserver/OverlaysObserver";
3
+ import { useFeedLoader } from "../../reactHooks";
4
+ import { usePlayer } from "./usePlayer";
5
+
6
+ export const useTitleSummaryOverlay = (
7
+ feedUrl,
8
+ entry,
9
+ isLive = false,
10
+ interval: number
11
+ ) => {
12
+ const player = usePlayer();
13
+ const [title, setTitle] = useState<string | number>(entry?.title || "");
14
+ const [summary, setSummary] = useState<string | number>(entry?.summary || "");
15
+
16
+ // Hook to load feed data
17
+ const { data: feedData, reloadData } = useFeedLoader({ feedUrl });
18
+
19
+ useEffect(() => {
20
+ if (player && feedUrl && isLive) {
21
+ const observer = new OverlaysObserver({ player });
22
+
23
+ observer.setFeedDataHandlers(feedUrl, reloadData, interval, (entry) => {
24
+ setTitle(entry?.title || "");
25
+ setSummary(entry?.summary || "");
26
+ });
27
+
28
+ const subscription = observer
29
+ .getTitleSummaryObservable()
30
+ .subscribe(({ title, summary }) => {
31
+ setTitle(title);
32
+ setSummary(summary);
33
+ });
34
+
35
+ return () => {
36
+ subscription.unsubscribe();
37
+ observer.clearFeedDataInterval();
38
+ };
39
+ }
40
+ }, [player, feedUrl, reloadData, isLive]);
41
+
42
+ useEffect(() => {
43
+ if (isLive && feedData?.entry?.[0]) {
44
+ setTitle(feedData.entry[0].title || "");
45
+ setSummary(feedData.entry[0].summary || "");
46
+ }
47
+ }, [feedData]);
48
+
49
+ return { title, summary };
50
+ };
@@ -14,6 +14,7 @@ import {
14
14
  castAndFallbackColor,
15
15
  compact,
16
16
  getValue,
17
+ getColorFromData,
17
18
  } from "../";
18
19
 
19
20
  import {
@@ -388,3 +389,50 @@ describe("getValue", () => {
388
389
  expect(getValue({ value })(value)).toBeNull();
389
390
  });
390
391
  });
392
+
393
+ describe("getColorFromData", () => {
394
+ it("should return value directly if valid color", () => {
395
+ const result = getColorFromData({
396
+ data: entry,
397
+ valueFromLayout: "#123456",
398
+ });
399
+
400
+ expect(result).toBe("#123456");
401
+ });
402
+
403
+ it("should return valid rgba color name", () => {
404
+ const result = getColorFromData({
405
+ data: entry,
406
+ valueFromLayout: "rgba(255, 255, 255, 0)",
407
+ });
408
+
409
+ expect(result).toBe("rgba(255, 255, 255, 0)");
410
+ });
411
+
412
+ it("should return path value if path value is set and is valid color", () => {
413
+ const result = getColorFromData({
414
+ data: entry,
415
+ valueFromLayout: "background_color",
416
+ });
417
+
418
+ expect(result).toBe("#123456");
419
+ });
420
+
421
+ it("should return path value if path value is set, nested and is valid color", () => {
422
+ const result = getColorFromData({
423
+ data: entry,
424
+ valueFromLayout: "extensions.background_color",
425
+ });
426
+
427
+ expect(result).toBe("#123456");
428
+ });
429
+
430
+ it("should return valueFromLayout if path does not exist", () => {
431
+ const result = getColorFromData({
432
+ data: entry,
433
+ valueFromLayout: "invalid_color",
434
+ });
435
+
436
+ expect(result).toBe("invalid_color");
437
+ });
438
+ });
@@ -1,5 +1,7 @@
1
1
  /* eslint-disable max-len */
2
2
 
3
+ export const defaultColor = "#ffffff";
4
+
3
5
  export const entry = {
4
6
  type: {
5
7
  value: "video",
@@ -26,7 +28,9 @@ export const entry = {
26
28
  type: "video/mp4",
27
29
  src: "https://wino-feed.s3.amazonaws.com/movieFeed/Ford+v+Ferrari.mp4",
28
30
  },
31
+ background_color: "#123456",
29
32
  extensions: {
33
+ background_color: "#123456",
30
34
  free: false,
31
35
  duration: 9000,
32
36
  section: "New Releases",
@@ -46,7 +50,11 @@ export const componentStyle = {
46
50
  horizontal_list_cell_width: "400",
47
51
  cell_plugin_configuration_id: "267bd05a-08bd-4675-bdd3-9b73899773f0",
48
52
  horizontal_list_scaled_switch: false,
53
+ background_color: "#123456",
49
54
  component_background_color: "rgba(255, 255, 255, 0)",
55
+ background_color_1: "extensions.background_color",
56
+ background_color_2: "invalidColorKey",
57
+ background_color_3: "extensions.invalidColorKeyPath",
50
58
  component_padding_left: "",
51
59
  horizontal_list_next_offset: 20,
52
60
  component_margin_bottom: "",
@@ -93,6 +101,11 @@ export const VAL = {
93
101
  upper: "STRING",
94
102
  width: 400,
95
103
  height: 225,
104
+ colorKey: "background_color",
105
+ colorKeyRgba: "component_background_color",
106
+ colorKeyPath: "background_color_1",
107
+ invalidColorKey: "background_color_2",
108
+ invalidColorKeyPath: "background_color_3",
96
109
  };
97
110
 
98
111
  export const TRANSFORM = {
@@ -1,5 +1,6 @@
1
1
  import * as R from "ramda";
2
2
  import dayjs from "dayjs";
3
+ import validateColor from "validate-color";
3
4
 
4
5
  import { toNumberWithDefault, toNumberWithDefaultZero } from "../numberUtils";
5
6
  import { transformColorCode as fixColorHexCode } from "../transform";
@@ -181,8 +182,8 @@ export const getHeight = (width, aspectRatio) => {
181
182
  * @returns {string} duration
182
183
  */
183
184
 
184
- export const getDurationInMinutes = (seconds: number): string => {
185
- if (seconds === (null || undefined)) return "00:00";
185
+ export const getDurationInMinutes = (seconds?: number): string => {
186
+ if (!seconds) return "00:00";
186
187
  const minutes = Math.floor(Number(seconds) / 60);
187
188
  const secondsReminder = Math.floor(Number(seconds)) % 60;
188
189
  const pad = (num: number): string => `${num < 10 ? "0" : ""}${num}`;
@@ -343,6 +344,19 @@ export const ifElseFocused = R.curry(
343
344
  state === "focused" ? focusedValue : defaultValue
344
345
  );
345
346
 
347
+ export const ifElseSelected = R.curry(
348
+ (state: string, selectedValue, defaultValue) => {
349
+ switch (state) {
350
+ case "selected":
351
+ return selectedValue;
352
+ case "focused_selected":
353
+ return selectedValue;
354
+ default:
355
+ return defaultValue;
356
+ }
357
+ }
358
+ );
359
+
346
360
  /**
347
361
  * Map textAlign values to their corresponding alignSelf values
348
362
  * This is so that we could use the same field for both properties
@@ -430,3 +444,27 @@ export const toResizeMode = (
430
444
  resizeMode: "contain",
431
445
  };
432
446
  };
447
+
448
+ type GetColorFromData = {
449
+ data: Record<string, any>;
450
+ valueFromLayout: string;
451
+ };
452
+
453
+ export const getColorFromData = ({
454
+ data,
455
+ valueFromLayout,
456
+ }: GetColorFromData): string => {
457
+ // Temporary hack to fix color validation when alpha is floating point number
458
+ // https://github.com/dreamyguy/validate-color/issues/44
459
+ if (validateColor(valueFromLayout.replace(".00", ""))) {
460
+ return valueFromLayout;
461
+ }
462
+
463
+ const pathValue = R.path(valueFromLayout.split("."), data);
464
+
465
+ if (pathValue && validateColor(pathValue)) {
466
+ return pathValue;
467
+ }
468
+
469
+ return valueFromLayout;
470
+ };
@@ -0,0 +1,11 @@
1
+ import dayjs from "dayjs";
2
+ import utc from "dayjs/plugin/utc";
3
+ import timezone from "dayjs/plugin/timezone";
4
+ import isoWeek from "dayjs/plugin/isoWeek";
5
+
6
+ // Extend dayjs with plugins
7
+ dayjs.extend(utc);
8
+ dayjs.extend(timezone);
9
+ dayjs.extend(isoWeek);
10
+
11
+ export { dayjs };
@@ -86,7 +86,7 @@ export const isFocusable = (component: ZappEntry | ZappUIComponent | any) =>
86
86
 
87
87
  type Props = {
88
88
  component: ZappUIComponent;
89
- zappPipesData: ZappPipesData;
89
+ zappPipesData?: ZappPipesData;
90
90
  } & Record<string, any>;
91
91
 
92
92
  export const useIsFocusable = (props: Props): void => {
@@ -1,6 +1,6 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`subscriber can be invoked without args 1`] = `
3
+ exports[`functionUtils subscriber can be invoked without args 1`] = `
4
4
  Object {
5
5
  "handlers": Object {},
6
6
  "invokeHandler": [Function],
@@ -9,7 +9,7 @@ Object {
9
9
  }
10
10
  `;
11
11
 
12
- exports[`subscriber returns the original object with additional methods 1`] = `
12
+ exports[`functionUtils subscriber returns the original object with additional methods 1`] = `
13
13
  Object {
14
14
  "foo": "bar",
15
15
  "handlers": Object {},