@applicaster/zapp-react-native-utils 16.0.0-alpha.6593152532 → 16.0.0-alpha.8443457654

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.
@@ -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;
@@ -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;
@@ -0,0 +1,150 @@
1
+ const mockAppStoreGet = jest.fn();
2
+ const mockFindPluginByIdentifier = jest.fn();
3
+
4
+ jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
5
+ appStore: { get: (key: string) => mockAppStoreGet(key) },
6
+ }));
7
+
8
+ jest.mock("@applicaster/zapp-react-native-utils/pluginUtils", () => ({
9
+ findPluginByIdentifier: (id: string, plugins: any) =>
10
+ mockFindPluginByIdentifier(id, plugins),
11
+ }));
12
+
13
+ import { PlayerRole } from "../conts";
14
+ import { playerFactory } from "../playerFactory";
15
+
16
+ const fakeComponent = () => null;
17
+
18
+ class FakeController {
19
+ receivedConfig: any;
20
+ constructor(config: any) {
21
+ this.receivedConfig = config;
22
+ }
23
+ }
24
+
25
+ const baseConfig = {
26
+ player: {},
27
+ playerId: "p1",
28
+ autoplay: false,
29
+ entry: { id: "entry-1" } as any,
30
+ muted: false,
31
+ playerPluginId: "test-plugin",
32
+ screenConfig: { foo: "bar" },
33
+ playerRole: PlayerRole.Cell,
34
+ };
35
+
36
+ const installPluginWithProtocol = (
37
+ protocolReturn: any | ((screenConfig: any, entry: any) => any)
38
+ ) => {
39
+ const playerProtocol =
40
+ typeof protocolReturn === "function"
41
+ ? protocolReturn
42
+ : () => protocolReturn;
43
+
44
+ mockAppStoreGet.mockImplementation((key: string) =>
45
+ key === "plugins" ? { "test-plugin": {} } : null
46
+ );
47
+
48
+ mockFindPluginByIdentifier.mockReturnValue({
49
+ module: { playerProtocol },
50
+ });
51
+ };
52
+
53
+ describe("playerFactory", () => {
54
+ beforeEach(() => {
55
+ mockAppStoreGet.mockReset();
56
+ mockFindPluginByIdentifier.mockReset();
57
+ });
58
+
59
+ it("resolves with controller and Component when playerProtocol returns synchronously", async () => {
60
+ installPluginWithProtocol({
61
+ Component: fakeComponent,
62
+ controllerClass: FakeController,
63
+ });
64
+
65
+ const item = await playerFactory(baseConfig);
66
+
67
+ expect(item).not.toBeNull();
68
+ expect(item?.Component).toBe(fakeComponent);
69
+ expect(item?.controller).toBeInstanceOf(FakeController);
70
+
71
+ expect((item?.controller as FakeController).receivedConfig).toBe(
72
+ baseConfig
73
+ );
74
+ });
75
+
76
+ it("resolves with controller and Component when playerProtocol returns a Promise (delayed import)", async () => {
77
+ installPluginWithProtocol(async () => ({
78
+ Component: fakeComponent,
79
+ controllerClass: FakeController,
80
+ }));
81
+
82
+ const item = await playerFactory(baseConfig);
83
+
84
+ expect(item).not.toBeNull();
85
+ expect(item?.Component).toBe(fakeComponent);
86
+ expect(item?.controller).toBeInstanceOf(FakeController);
87
+ });
88
+
89
+ it("passes screenConfig and entry through to playerProtocol", async () => {
90
+ const playerProtocol = jest.fn(() => ({
91
+ Component: fakeComponent,
92
+ controllerClass: FakeController,
93
+ }));
94
+
95
+ mockAppStoreGet.mockImplementation((key: string) =>
96
+ key === "plugins" ? { "test-plugin": {} } : null
97
+ );
98
+
99
+ mockFindPluginByIdentifier.mockReturnValue({ module: { playerProtocol } });
100
+
101
+ await playerFactory(baseConfig);
102
+
103
+ expect(playerProtocol).toHaveBeenCalledWith(
104
+ baseConfig.screenConfig,
105
+ baseConfig.entry
106
+ );
107
+ });
108
+
109
+ it("resolves to null when playerPluginId is missing", async () => {
110
+ expect(
111
+ await playerFactory({ ...baseConfig, playerPluginId: "" } as any)
112
+ ).toBeNull();
113
+ });
114
+
115
+ it("resolves to null when appStore has no plugins", async () => {
116
+ mockAppStoreGet.mockReturnValue(null);
117
+
118
+ expect(await playerFactory(baseConfig)).toBeNull();
119
+ });
120
+
121
+ it("resolves to null when the plugin has no playerProtocol", async () => {
122
+ mockAppStoreGet.mockImplementation((key: string) =>
123
+ key === "plugins" ? { "test-plugin": {} } : null
124
+ );
125
+
126
+ mockFindPluginByIdentifier.mockReturnValue({ module: {} });
127
+
128
+ expect(await playerFactory(baseConfig)).toBeNull();
129
+ });
130
+
131
+ it("resolves to null when playerProtocol returns without a Component", async () => {
132
+ installPluginWithProtocol({ controllerClass: FakeController });
133
+
134
+ expect(await playerFactory(baseConfig)).toBeNull();
135
+ });
136
+
137
+ it("resolves to null when playerProtocol returns without a controllerClass", async () => {
138
+ installPluginWithProtocol({ Component: fakeComponent });
139
+
140
+ expect(await playerFactory(baseConfig)).toBeNull();
141
+ });
142
+
143
+ it("rejects when playerProtocol throws synchronously", async () => {
144
+ installPluginWithProtocol(() => {
145
+ throw new Error("boom");
146
+ });
147
+
148
+ await expect(playerFactory(baseConfig)).rejects.toThrow("boom");
149
+ });
150
+ });
@@ -19,52 +19,35 @@ type PlayerFactoryProps = {
19
19
  playerRole: PlayerRole;
20
20
  };
21
21
 
22
- interface PlayerConstructor {
22
+ interface PlayerControllerConstructor {
23
23
  new (params: any): Player;
24
24
  }
25
25
 
26
- export const playerFactory = (
27
- config: PlayerFactoryProps
28
- ): PlayerFactoryItem | null => {
29
- const IDENTIFIER = config?.playerPluginId;
26
+ type PlayerProtocol = {
27
+ Component: any;
28
+ controllerClass: PlayerControllerConstructor;
29
+ };
30
30
 
31
- if (!IDENTIFIER) {
32
- return null;
33
- }
31
+ export const playerFactory = async (
32
+ config: PlayerFactoryProps
33
+ ): Promise<PlayerFactoryItem | null> => {
34
+ if (!config?.playerPluginId) return null;
34
35
 
35
36
  const plugins = appStore.get("plugins");
37
+ if (!plugins) return null;
36
38
 
37
- if (!plugins) {
38
- return null;
39
- }
40
-
41
- const playerPlugin = findPluginByIdentifier(IDENTIFIER, plugins);
39
+ const playerPlugin = findPluginByIdentifier(config.playerPluginId, plugins);
40
+ if (!playerPlugin?.module?.playerProtocol) return null;
42
41
 
43
- if (!playerPlugin?.module?.playerProtocol) {
44
- return null;
45
- }
46
-
47
- const playerProtocol: PlayerConstructor = playerPlugin.module.playerProtocol(
48
- config?.screenConfig,
49
- config?.entry
50
- );
42
+ const playerProtocol: PlayerProtocol | null =
43
+ await playerPlugin.module.playerProtocol(config.screenConfig, config.entry);
51
44
 
52
- const playerView = (playerProtocol as any)?.Component || null;
53
-
54
- if (!playerView) {
45
+ if (!playerProtocol?.Component || !playerProtocol.controllerClass) {
55
46
  return null;
56
47
  }
57
48
 
58
- const Controller = (playerProtocol as any)?.controllerClass || null;
59
-
60
- if (Controller === null) {
61
- return null;
62
- }
63
-
64
- const controller = new Controller(config);
65
-
66
49
  return {
67
- controller,
68
- Component: playerView,
50
+ controller: new playerProtocol.controllerClass(config),
51
+ Component: playerProtocol.Component,
69
52
  };
70
53
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "16.0.0-alpha.6593152532",
3
+ "version": "16.0.0-alpha.8443457654",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "16.0.0-alpha.6593152532",
30
+ "@applicaster/applicaster-types": "16.0.0-alpha.8443457654",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -9,6 +9,16 @@ import { Dimensions } from "react-native";
9
9
 
10
10
  export { getPlayerActionButtons } from "./getPlayerActionButtons";
11
11
 
12
+ export const HLS_MIME_TYPES = [
13
+ "application/x-mpegURL",
14
+ "application/vnd.apple.mpegurl",
15
+ ];
16
+
17
+ export const STREAMING_VIDEO_MIME_TYPES = [
18
+ ...HLS_MIME_TYPES,
19
+ "application/dash+xml",
20
+ ];
21
+
12
22
  /**
13
23
  * Gets duration value from player manager, and from extensions
14
24
  * then checks whether the value from either is a not a valid number
@@ -27,7 +37,7 @@ export function isLiveLegacy(content) {
27
37
  ]);
28
38
 
29
39
  const durationFromExt = R.path(["extensions", "duration"], content);
30
- const durationFromMgr = playerManager.getDuration();
40
+ const durationFromMgr = playerManager.getInstanceController()?.getDuration();
31
41
  const duration = Math.floor(durationFromExt || durationFromMgr);
32
42
 
33
43
  const isLive = R.anyPass([isNotaValidNumber, R.lte(R.__, 0)])(duration);
@@ -71,7 +81,9 @@ function isLiveByManager(): boolean {
71
81
  return true;
72
82
  }
73
83
 
74
- const durationFromPlayerManager = playerManager.getDuration();
84
+ const durationFromPlayerManager = playerManager
85
+ .getInstanceController()
86
+ ?.getDuration();
75
87
 
76
88
  return isLiveByDuration(durationFromPlayerManager);
77
89
  }
@@ -80,6 +92,16 @@ export function isLive(entry: ZappEntry): boolean {
80
92
  return isEntryLive(entry) || isLiveByManager();
81
93
  }
82
94
 
95
+ export const isVideoItem = (item: Option<ZappEntry>) => {
96
+ const contentType = item?.content?.type;
97
+
98
+ return (
99
+ isString(contentType) &&
100
+ (contentType.includes("video") ||
101
+ STREAMING_VIDEO_MIME_TYPES.includes(contentType))
102
+ );
103
+ };
104
+
83
105
  export const isAudioItem = (item: Option<ZappEntry>) => {
84
106
  if (
85
107
  isString(item?.content?.type) &&
@@ -0,0 +1,114 @@
1
+ import { renderHook } from "@testing-library/react-native";
2
+ import { useIsStandaloneFullscreen } from "../useIsStandaloneFullscreen";
3
+
4
+ import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
5
+ import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
6
+
7
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks");
8
+ jest.mock("@applicaster/zapp-react-native-utils/booleanUtils");
9
+
10
+ const mockUseNavigation = useNavigation as jest.Mock;
11
+ const mockToBoolean = toBooleanWithDefaultFalse as jest.Mock;
12
+
13
+ describe("useIsStandaloneFullscreen", () => {
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+
17
+ mockUseNavigation.mockReturnValue({
18
+ canGoBack: jest.fn().mockReturnValue(false),
19
+ screenData: {
20
+ general: { allow_screen_plugin_presentation: true },
21
+ },
22
+ });
23
+
24
+ mockToBoolean.mockReturnValue(false);
25
+ });
26
+
27
+ it("returns true when cannot go back and screen plugin presentation is allowed", () => {
28
+ mockToBoolean.mockReturnValue(true);
29
+
30
+ const { result } = renderHook(() => useIsStandaloneFullscreen());
31
+
32
+ expect(mockToBoolean).toHaveBeenCalledWith(true);
33
+ expect(result.current).toBe(true);
34
+ });
35
+
36
+ it("returns false when can go back even if screen plugin presentation is allowed", () => {
37
+ mockUseNavigation.mockReturnValue({
38
+ canGoBack: jest.fn().mockReturnValue(true),
39
+ screenData: {
40
+ general: { allow_screen_plugin_presentation: true },
41
+ },
42
+ });
43
+
44
+ mockToBoolean.mockReturnValue(false);
45
+
46
+ const { result } = renderHook(() => useIsStandaloneFullscreen());
47
+
48
+ expect(mockToBoolean).toHaveBeenCalledWith(false);
49
+ expect(result.current).toBe(false);
50
+ });
51
+
52
+ it("returns false when cannot go back but screen plugin presentation is not allowed", () => {
53
+ mockUseNavigation.mockReturnValue({
54
+ canGoBack: jest.fn().mockReturnValue(false),
55
+ screenData: {
56
+ general: { allow_screen_plugin_presentation: false },
57
+ },
58
+ });
59
+
60
+ mockToBoolean.mockReturnValue(false);
61
+
62
+ const { result } = renderHook(() => useIsStandaloneFullscreen());
63
+
64
+ expect(mockToBoolean).toHaveBeenCalledWith(false);
65
+ expect(result.current).toBe(false);
66
+ });
67
+
68
+ it("returns false when screen plugin presentation flag is undefined", () => {
69
+ mockUseNavigation.mockReturnValue({
70
+ canGoBack: jest.fn().mockReturnValue(false),
71
+ screenData: { general: {} },
72
+ });
73
+
74
+ mockToBoolean.mockReturnValue(false);
75
+
76
+ const { result } = renderHook(() => useIsStandaloneFullscreen());
77
+
78
+ expect(mockToBoolean).toHaveBeenCalledWith(undefined);
79
+ expect(result.current).toBe(false);
80
+ });
81
+
82
+ it("returns false when navigator is undefined", () => {
83
+ mockUseNavigation.mockReturnValue(undefined);
84
+ mockToBoolean.mockReturnValue(false);
85
+
86
+ const { result } = renderHook(() => useIsStandaloneFullscreen());
87
+
88
+ expect(mockToBoolean).toHaveBeenCalledWith(undefined);
89
+ expect(result.current).toBe(false);
90
+ });
91
+
92
+ it("returns false when screenData is undefined", () => {
93
+ mockUseNavigation.mockReturnValue({
94
+ canGoBack: jest.fn().mockReturnValue(false),
95
+ screenData: undefined,
96
+ });
97
+
98
+ mockToBoolean.mockReturnValue(false);
99
+
100
+ const { result } = renderHook(() => useIsStandaloneFullscreen());
101
+
102
+ expect(mockToBoolean).toHaveBeenCalledWith(undefined);
103
+ expect(result.current).toBe(false);
104
+ });
105
+
106
+ it("passes the combined condition through toBooleanWithDefaultFalse", () => {
107
+ mockToBoolean.mockImplementation((val) => Boolean(val));
108
+
109
+ const { result } = renderHook(() => useIsStandaloneFullscreen());
110
+
111
+ expect(mockToBoolean).toHaveBeenCalledWith(true);
112
+ expect(result.current).toBe(true);
113
+ });
114
+ });
@@ -16,3 +16,5 @@ export { useScreenBackgroundColor } from "./useScreenBackgroundColor";
16
16
  export { useCurrentScreenIsHook } from "./useCurrentScreenIsHook";
17
17
 
18
18
  export { useCurrentScreenIsStartupHook } from "./useCurrentScreenIsStartupHook";
19
+
20
+ export { useIsStandaloneFullscreen } from "./useIsStandaloneFullscreen";
@@ -0,0 +1,12 @@
1
+ import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
2
+ import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
3
+
4
+ export const useIsStandaloneFullscreen = (): boolean => {
5
+ const navigator = useNavigation();
6
+
7
+ return toBooleanWithDefaultFalse(
8
+ !navigator?.canGoBack() &&
9
+ // @ts-ignore
10
+ navigator?.screenData?.general?.allow_screen_plugin_presentation
11
+ );
12
+ };
@@ -2,6 +2,12 @@ import React from "react";
2
2
  import { useContentTypes } from "@applicaster/zapp-react-native-redux/hooks";
3
3
  import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
4
4
  import { useRivers } from "../../state";
5
+ import { createLogger } from "../../../logger";
6
+
7
+ const { log_warning } = createLogger({
8
+ subsystem: "zapp-react-native-utils/reactHooks/videoModal",
9
+ category: "useVideoModalScreenData",
10
+ });
5
11
 
6
12
  export const useVideoModalScreenData = ():
7
13
  | (ZappEntry & { targetScreen: any }) // TODO: fix ZappEntry type ( was ZappRiver but conflict )
@@ -14,10 +20,22 @@ export const useVideoModalScreenData = ():
14
20
  const rivers = useRivers();
15
21
 
16
22
  return React.useMemo(() => {
17
- if (item?.type?.value) {
18
- const screenId = contentTypes[item?.type?.value].screen_id;
23
+ const itemType = item?.type?.value;
24
+
25
+ if (!itemType) {
26
+ return;
27
+ }
19
28
 
20
- return { ...item, targetScreen: rivers?.[screenId] };
29
+ const screenId = contentTypes[itemType]?.screen_id;
30
+
31
+ if (!screenId) {
32
+ log_warning(
33
+ `Type mapping is missing for item type: ${itemType}, title: ${item.title}`
34
+ );
35
+
36
+ return;
21
37
  }
22
- }, [item]);
38
+
39
+ return { ...item, targetScreen: rivers?.[screenId] };
40
+ }, [contentTypes, item, rivers]);
23
41
  };
@@ -197,7 +197,7 @@ export const useCallbackNavigationAction = (
197
197
  }
198
198
  }
199
199
 
200
- hookCallback?.({ ...args, success: false, cancelled: true });
200
+ hookCallback?.({ ...args, success: false, abort: true });
201
201
  const currentNavigation = navigationRef.current;
202
202
 
203
203
  switch (data.action) {