@applicaster/zapp-react-native-utils 13.0.0-alpha.2807014304 → 13.0.0-alpha.2957047383

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 (32) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +104 -6
  2. package/appUtils/HooksManager/Hook.ts +4 -0
  3. package/appUtils/HooksManager/index.ts +2 -0
  4. package/appUtils/contextKeysManager/contextResolver.ts +1 -12
  5. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +1 -0
  6. package/appUtils/focusManager/index.ts +13 -0
  7. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +7 -1
  8. package/dateUtils/index.ts +2 -0
  9. package/focusManager/FocusManager.ts +3 -1
  10. package/focusManager/index.ts +4 -30
  11. package/functionUtils/index.ts +25 -0
  12. package/index.d.ts +0 -12
  13. package/manifestUtils/defaultManifestConfigurations/player.js +6 -0
  14. package/modalState/__tests__/index.test.ts +51 -0
  15. package/modalState/index.ts +32 -2
  16. package/navigationUtils/__tests__/__snapshots__/navigationUtils.test.js.snap +1 -0
  17. package/navigationUtils/index.ts +3 -1
  18. package/numberUtils/__tests__/toPositiveNumberWithDefault.test.ts +23 -3
  19. package/numberUtils/index.ts +4 -0
  20. package/package.json +2 -2
  21. package/playerUtils/configurationGenerator.ts +6 -0
  22. package/reactHooks/advertising/index.ts +5 -5
  23. package/reactHooks/feed/useFeedLoader.tsx +17 -8
  24. package/reactHooks/hookModal/hooks/useHookModalScreenData.ts +39 -0
  25. package/reactHooks/navigation/useRoute.ts +4 -6
  26. package/utils/README.md +30 -0
  27. package/utils/chunk.ts +5 -0
  28. package/utils/index.ts +5 -0
  29. package/utils/times.ts +5 -0
  30. package/zappFrameworkUtils/localStorageHelper.ts +6 -0
  31. package/zappFrameworkUtils/loginPluginUtils.ts +6 -2
  32. package/zappFrameworkUtils/types.ts +11 -0
@@ -1,6 +1,6 @@
1
1
  /// <reference types="@applicaster/applicaster-types" />
2
2
  import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
3
- import React, { createContext, useMemo } from "react";
3
+ import React, { createContext, useEffect, useMemo } from "react";
4
4
  import {
5
5
  actionExecutor as _actionExecutor,
6
6
  ActionResult,
@@ -19,8 +19,15 @@ import {
19
19
  EntryResolver,
20
20
  resolveObjectValues,
21
21
  } from "../appUtils/contextKeysManager/contextResolver";
22
+ import { useNavigation } from "../reactHooks";
23
+ import { get } from "lodash";
22
24
 
23
- const { log_error, log_info } = createLogger({
25
+ import {
26
+ useContentTypes,
27
+ usePickFromState,
28
+ } from "@applicaster/zapp-react-native-redux/hooks";
29
+
30
+ export const { log_error, log_info, log_debug } = createLogger({
24
31
  subsystem: "ActionExecutorContext",
25
32
  category: "General",
26
33
  });
@@ -51,6 +58,22 @@ type Props = {
51
58
  children: React.ReactNode;
52
59
  };
53
60
 
61
+ function findParentComponent(
62
+ childId: string,
63
+ parent: ZappRiver | ZappUIComponent
64
+ ): ZappRiver | ZappUIComponent | null {
65
+ for (const child of parent.ui_components) {
66
+ if (child.id === childId) return parent;
67
+
68
+ if (child.ui_components) {
69
+ const found = findParentComponent(childId, child);
70
+ if (found) return found;
71
+ }
72
+ }
73
+
74
+ return null;
75
+ }
76
+
54
77
  const prepareDefaultActions = (actionExecutor) => {
55
78
  actionExecutor.registerAction("localStorageSet", async (action) => {
56
79
  const namespaces = action.options.content;
@@ -65,11 +88,13 @@ const prepareDefaultActions = (actionExecutor) => {
65
88
  async (_action, context) => {
66
89
  const dispatch = appStore.getDispatch();
67
90
 
68
- log_info("handleAction: refreshComponent event");
91
+ const dataSource =
92
+ context?.component?.data?.source ||
93
+ findParentComponent(context?.component.id, context?.screenData)?.data
94
+ ?.source;
69
95
 
70
- const dataSource = context?.component?.data?.source;
96
+ log_info(`handleAction: refreshComponent for dataSource:${dataSource}`);
71
97
 
72
- // TODO: Later we can pass component ids to do refresh instead of current one
73
98
  // TODO: In theory we should wait callback to complete, before completing the action, but now it's not needed
74
99
  // TODO: handle focused item removal
75
100
  dispatch(
@@ -169,8 +194,29 @@ const prepareDefaultActions = (actionExecutor) => {
169
194
  action: ActionType,
170
195
  context?: Record<string, any>
171
196
  ): Promise<ActionResult> => {
197
+ if (!context) {
198
+ log_error(
199
+ "handleAction: localStorageToggleFlag action missing context"
200
+ );
201
+
202
+ return ActionResult.Error;
203
+ }
204
+
205
+ const entry = context?.entry as ZappEntry;
206
+
207
+ if (!entry) {
208
+ log_error(
209
+ "handleAction: localStorageToggleFlag action missing entry. Entry is required to get the tag."
210
+ );
211
+
212
+ return ActionResult.Error;
213
+ }
214
+
215
+ const tag = action.options?.selector
216
+ ? get(entry, action.options.selector)
217
+ : (entry.extensions?.tag ?? entry.id);
218
+
172
219
  const keyNamespace = action.options?.key;
173
- const tag = context?.entry?.extensions?.tag;
174
220
 
175
221
  if (keyNamespace && tag) {
176
222
  const multiSelectProvider =
@@ -188,6 +234,16 @@ const prepareDefaultActions = (actionExecutor) => {
188
234
  if (selectedItems.includes(tag)) {
189
235
  await multiSelectProvider.removeItem(tag);
190
236
  } else {
237
+ const maxItems = action.options?.max_items;
238
+
239
+ if (maxItems && selectedItems.length >= maxItems) {
240
+ log_info(
241
+ `handleAction: localStorageToggleFlag event reached max items limit: ${maxItems}, cannot add tag: ${tag}`
242
+ );
243
+
244
+ return ActionResult.Cancel;
245
+ }
246
+
191
247
  await multiSelectProvider.addItem(tag);
192
248
  }
193
249
  } else {
@@ -211,6 +267,10 @@ export function withActionExecutor(Component) {
211
267
  prepareDefaultActions(_actionExecutor);
212
268
 
213
269
  return function ActionExecutorComponent(props: Props) {
270
+ const navigator = useNavigation();
271
+ const { rivers } = usePickFromState(["rivers"]);
272
+ const contentTypes = useContentTypes();
273
+
214
274
  const handlers = useMemo(() => {
215
275
  return {
216
276
  unregisterAction: _actionExecutor.unregisterAction,
@@ -221,6 +281,44 @@ export function withActionExecutor(Component) {
221
281
  };
222
282
  }, []);
223
283
 
284
+ useEffect(() => {
285
+ return _actionExecutor.registerAction(
286
+ "navigateToScreen",
287
+ async (action: ActionType, context?: Record<string, any>) => {
288
+ const screenType = action.options?.typeMapping;
289
+
290
+ if (!screenType) {
291
+ log_error("navigateToScreen: typeMapping option is missing");
292
+
293
+ return ActionResult.Error;
294
+ }
295
+
296
+ const screenId = contentTypes?.[screenType]?.screen_id || null;
297
+
298
+ if (!screenId) {
299
+ log_error(
300
+ `navigateToScreen: can not resolve screen type mapping: ${screenType}`
301
+ );
302
+
303
+ return ActionResult.Error;
304
+ }
305
+
306
+ const river = rivers[screenId];
307
+
308
+ if (!river) {
309
+ log_error("navigateToScreen: can not resolve river");
310
+
311
+ return ActionResult.Error;
312
+ }
313
+
314
+ context?.callback?.({ success: false, error: null, abort: true });
315
+ navigator.replace(river);
316
+
317
+ return ActionResult.Success;
318
+ }
319
+ );
320
+ }, [navigator, rivers, contentTypes]);
321
+
224
322
  return (
225
323
  <ActionExecutorContext.Provider value={handlers}>
226
324
  <Component {...props} />
@@ -65,6 +65,10 @@ export class Hook implements HookInterface {
65
65
  event: (typeof HOOKS_EVENTS)[keyof typeof HOOKS_EVENTS],
66
66
  ...args
67
67
  ) {
68
+ if (this.state === hookState(HOOKS_EVENTS.CANCEL)) {
69
+ return;
70
+ }
71
+
68
72
  this.state = hookState(event);
69
73
  this.manager.subscriber.invokeHandler(event, ...args);
70
74
  }
@@ -269,6 +269,7 @@ export function HooksManager({
269
269
  payload = initialPayload,
270
270
  callback: callbackArg,
271
271
  cancelled,
272
+ abort,
272
273
  }) {
273
274
  let callback = callbackArg;
274
275
 
@@ -308,6 +309,7 @@ export function HooksManager({
308
309
  hookPlugin.setStateAndNotify(HOOKS_EVENTS.CANCEL, {
309
310
  hookPlugin,
310
311
  payload,
312
+ abort,
311
313
  });
312
314
 
313
315
  const isHookInHomescreen = payload?.home;
@@ -1,12 +1,6 @@
1
- import { createLogger } from "../../logger";
2
1
  import { ContextKeysManager } from "./index";
3
2
  import * as R from "ramda";
4
3
 
5
- const { log_error } = createLogger({
6
- subsystem: "contextResolver",
7
- category: "General",
8
- });
9
-
10
4
  interface IResolver {
11
5
  resolve: (string) => Promise<string | number | object>;
12
6
  }
@@ -64,12 +58,7 @@ export const resolveObjectValues = async (
64
58
  return [key, resolvedValue];
65
59
  }
66
60
 
67
- log_error("resolveObjectValues: invalid context string", {
68
- key,
69
- value,
70
- });
71
-
72
- return [key, null];
61
+ return [key, value];
73
62
  })
74
63
  );
75
64
 
@@ -5,6 +5,7 @@ exports[`focusManager should be defined 1`] = `
5
5
  "blur": [Function],
6
6
  "disableFocus": [Function],
7
7
  "enableFocus": [Function],
8
+ "findPreferredFocusChild": [Function],
8
9
  "focusableTree": Tree {
9
10
  "loadingCounter": 0,
10
11
  "root": {
@@ -534,6 +534,18 @@ export const focusManager = (function () {
534
534
 
535
535
  const { on, invokeHandler, removeHandler } = eventHandler;
536
536
 
537
+ function findPreferredFocusChild(id) {
538
+ const node = getCurrentFocusNode(id);
539
+
540
+ const preferredFocus = findPriorityItem([node]);
541
+
542
+ if (isNilOrEmpty(preferredFocus)) {
543
+ return undefined;
544
+ }
545
+
546
+ return preferredFocus[0];
547
+ }
548
+
537
549
  /**
538
550
  * this is the list of the functions available externally
539
551
  * when importing the focus manager
@@ -563,5 +575,6 @@ export const focusManager = (function () {
563
575
  isGroupItemFocused,
564
576
  recoverFocus,
565
577
  isCurrentFocusOnTheTopScreen,
578
+ findPreferredFocusChild,
566
579
  };
567
580
  })();
@@ -3,12 +3,18 @@ import { OverlaysObserver } from "./OverlayObserver/OverlaysObserver";
3
3
  import { useFeedLoader } from "../../reactHooks";
4
4
  import { usePlayer } from "./usePlayer";
5
5
 
6
+ const getLiveUrl = (url: string) => {
7
+ // Use the replace method with a regular expression to replace all instances of subClass with livePage
8
+ return url.replace(new RegExp("subClass", "g"), "livePage");
9
+ };
10
+
6
11
  export const useTitleSummaryOverlay = (
7
- feedUrl,
8
12
  entry,
9
13
  isLive = false,
10
14
  interval: number
11
15
  ) => {
16
+ // TODO: Have generic place for live url from configuration. Currently its hardcoded for client.
17
+ const feedUrl = getLiveUrl(entry?.extensions?.live?.channel_src ?? "");
12
18
  const player = usePlayer();
13
19
  const [title, setTitle] = useState<string | number>(entry?.title || "");
14
20
  const [summary, setSummary] = useState<string | number>(entry?.summary || "");
@@ -2,10 +2,12 @@ import dayjs from "dayjs";
2
2
  import utc from "dayjs/plugin/utc";
3
3
  import timezone from "dayjs/plugin/timezone";
4
4
  import isoWeek from "dayjs/plugin/isoWeek";
5
+ import duration from "dayjs/plugin/duration";
5
6
 
6
7
  // Extend dayjs with plugins
7
8
  dayjs.extend(utc);
8
9
  dayjs.extend(timezone);
9
10
  dayjs.extend(isoWeek);
11
+ dayjs.extend(duration);
10
12
 
11
13
  export { dayjs };
@@ -227,7 +227,9 @@ class FocusManager {
227
227
 
228
228
  const nextNextFocus = FocusManager.getNextFocusable(direction, nextFocus);
229
229
 
230
- FocusManager.instance.setFocus(nextNextFocus, options);
230
+ if (nextNextFocus) {
231
+ FocusManager.instance.setFocus(nextNextFocus, options);
232
+ }
231
233
  } else {
232
234
  FocusManager.instance.setFocusLocal(nextFocus);
233
235
 
@@ -8,8 +8,6 @@ import { isNilOrEmpty } from "../reactUtils/helpers";
8
8
  import { isAndroidTVPlatform } from "../reactUtils";
9
9
  import { noop } from "../functionUtils";
10
10
 
11
- const EMPTY_GROUP_COMPONENT = "empty_group_component";
12
-
13
11
  export function useFocusManager() {
14
12
  return focusManager;
15
13
  }
@@ -76,14 +74,6 @@ export function useFocusEffect(onFocus, focusWatcher) {
76
74
  }, focusWatcher);
77
75
  }
78
76
 
79
- export function shouldShowEmptyGroup(component) {
80
- return (
81
- component?.ui_components?.filter(
82
- ({ component_type }) => component_type === EMPTY_GROUP_COMPONENT
83
- )?.length > 0
84
- );
85
- }
86
-
87
77
  export const isFocusable = (component: ZappEntry | ZappUIComponent | any) =>
88
78
  R.tryCatch(
89
79
  R.compose(
@@ -101,6 +91,7 @@ type Props = {
101
91
 
102
92
  export const useIsFocusable = (props: Props): void => {
103
93
  const { component, zappPipesData } = props;
94
+
104
95
  const { setIsFocusable } = useFocusable();
105
96
 
106
97
  const componentCellsFocusable = toBooleanWithDefaultTrue(
@@ -117,33 +108,18 @@ export const useIsFocusable = (props: Props): void => {
117
108
  isNilOrEmpty(zappPipesData?.data) ||
118
109
  isNilOrEmpty(zappPipesData?.data?.entry);
119
110
 
120
- const hasNoUIComponents = isNilOrEmpty(component?.ui_components);
121
-
122
111
  const hasDataError = !!zappPipesData?.error;
123
112
 
124
- const hasEmptyGroup = shouldShowEmptyGroup(component);
125
-
126
- const isGallery = component?.component_type === "gallery-qb";
127
-
128
- const isGroup = component?.component_type === "group-qb";
129
-
130
- const isHiddenDueToEmptyData =
131
- !isLoadingData && hasNoData && hideIfDataEmpty && !hasEmptyGroup;
132
-
133
- const isEmptyGroup = isGroup && hasNoUIComponents;
113
+ const isHiddenDueToEmptyData = !isLoadingData && hasNoData && hideIfDataEmpty;
134
114
 
135
115
  const defaultFocusableLogic =
136
- componentCellsFocusable &&
137
- (!hasNoData || !hideIfDataEmpty) &&
138
- !isEmptyGroup;
116
+ componentCellsFocusable && (!hasNoData || !hideIfDataEmpty);
139
117
 
140
118
  React.useEffect(() => {
141
119
  if (isAndroidTVPlatform()) {
142
120
  let isFocusableNew = false;
143
121
 
144
- if (isGallery) {
145
- isFocusableNew = true;
146
- } else if (isFocusable(component)) {
122
+ if (isFocusable(component)) {
147
123
  isFocusableNew = defaultFocusableLogic;
148
124
  }
149
125
 
@@ -152,9 +128,7 @@ export const useIsFocusable = (props: Props): void => {
152
128
  }, [
153
129
  defaultFocusableLogic,
154
130
  isHiddenDueToEmptyData,
155
- isGallery,
156
131
  setIsFocusable,
157
132
  hasDataError,
158
- hasEmptyGroup,
159
133
  ]);
160
134
  };
@@ -258,3 +258,28 @@ export const extractDataFromProperty = (property, data): any =>
258
258
  );
259
259
 
260
260
  export const then = (fn) => (promise) => promise.then(fn);
261
+
262
+ /**
263
+ * Returns a replacer function for JSON.stringify that handles circular references,
264
+ * functions, and React context objects
265
+ * @returns {Function} A replacer function that detects and handles circular references,
266
+ * functions, and React context objects
267
+ */
268
+ export const getJsonReplacer = () => {
269
+ return (_key, value) => {
270
+ if (typeof value === "function") {
271
+ return "[Function]";
272
+ }
273
+
274
+ if (
275
+ typeof value === "object" &&
276
+ value !== null &&
277
+ value.Provider &&
278
+ value.Consumer
279
+ ) {
280
+ return "[ReactContext]";
281
+ }
282
+
283
+ return value;
284
+ };
285
+ };
package/index.d.ts CHANGED
@@ -105,18 +105,6 @@ declare type NavBarState = {
105
105
  title: string;
106
106
  };
107
107
 
108
- declare type StorageValuesToAdd = Record<string, NamespaceValues>;
109
-
110
- declare type StorageValuesToRemove = {
111
- string?: string[];
112
- };
113
-
114
- declare type StorageOwnedValues = {
115
- string?: string[];
116
- };
117
-
118
- declare type NamespaceValues = Record<string, string>;
119
-
120
108
  declare type RefreshProps = {
121
109
  forceRefresh?: boolean;
122
110
  };
@@ -2555,6 +2555,12 @@ function getPlayerConfiguration({ platform }) {
2555
2555
  multiple: false,
2556
2556
  label_tooltip: "Choose the default aspect ratio for the artwork",
2557
2557
  },
2558
+ {
2559
+ key: "audio_player_artwork_border_radius",
2560
+ label: "Audio Player Artwork Corner Radius",
2561
+ type: "number_input",
2562
+ initial_value: 16,
2563
+ },
2558
2564
  ]
2559
2565
  )
2560
2566
  );
@@ -0,0 +1,51 @@
1
+ import { modalStore } from "../";
2
+
3
+ describe("Modal State", () => {
4
+ it("should have initial state", () => {
5
+ const state = modalStore.getState();
6
+
7
+ expect(state).toMatchObject({
8
+ modalState: {
9
+ visible: false,
10
+ screen: null,
11
+ options: {},
12
+ props: {},
13
+ },
14
+ });
15
+
16
+ expect(state.openModal).toBeDefined();
17
+ expect(state.dismissModal).toBeDefined();
18
+ });
19
+
20
+ describe("opening and dismissing modal", () => {
21
+ it("should open modal", () => {
22
+ const item = { id: "test", name: "Test Modal" };
23
+ const options = { animated: true, animationType: "slide" };
24
+ const props = { customProp: "value" };
25
+
26
+ modalStore.getState().openModal({ item, options, props });
27
+
28
+ const state = modalStore.getState();
29
+
30
+ expect(state.modalState).toMatchObject({
31
+ visible: true,
32
+ screen: item,
33
+ options,
34
+ props,
35
+ });
36
+ });
37
+
38
+ it("should dismiss modal", () => {
39
+ modalStore.getState().dismissModal();
40
+
41
+ const state = modalStore.getState();
42
+
43
+ expect(state.modalState).toMatchObject({
44
+ visible: false,
45
+ screen: null,
46
+ options: {},
47
+ props: {},
48
+ });
49
+ });
50
+ });
51
+ });
@@ -1,4 +1,4 @@
1
- import { create } from "zustand";
1
+ import { useStore, createStore } from "zustand";
2
2
 
3
3
  interface ModalState {
4
4
  visible: boolean;
@@ -25,7 +25,7 @@ const initialModalState: ModalState = {
25
25
  props: {},
26
26
  };
27
27
 
28
- export const useModalStore = create<ModalStore>((set) => ({
28
+ export const modalStore = createStore<ModalStore>((set) => ({
29
29
  modalState: initialModalState,
30
30
  setModalState: (newState) =>
31
31
  set((state) => ({
@@ -51,3 +51,33 @@ export const useModalStore = create<ModalStore>((set) => ({
51
51
  }),
52
52
  dismissModal: () => set(() => ({ modalState: initialModalState })),
53
53
  }));
54
+
55
+ export const useModalStore = <T>(selector: (state: ModalStore) => T) =>
56
+ useStore(modalStore, selector);
57
+
58
+ export const openModal = modalStore.getInitialState().openModal;
59
+
60
+ export const dismissModal = modalStore.getInitialState().dismissModal;
61
+
62
+ export const openBottomSheetModal = ({
63
+ ModalBottomSheetContent,
64
+ modalBottomSheetContentProps,
65
+ props = {},
66
+ options = {},
67
+ }: OpenModalBottomSheetArgs) => {
68
+ openModal({
69
+ item: {
70
+ ModalBottomSheetContent,
71
+ modalBottomSheetContentProps,
72
+ },
73
+ props,
74
+ options: {
75
+ animationType: "none",
76
+ animated: false,
77
+ ...options,
78
+ },
79
+ });
80
+ };
81
+
82
+ export const useModalStoreState = (): ModalState =>
83
+ useModalStore<ModalState>((state) => state.modalState);
@@ -5,6 +5,7 @@ exports[`getNavigationProps returns the props otherwise 1`] = `
5
5
  "assets": {
6
6
  "appLogo": "appLogo.png",
7
7
  },
8
+ "data": undefined,
8
9
  "home": false,
9
10
  "nav_items": [
10
11
  {
@@ -111,6 +111,7 @@ export function getNavigationProps({
111
111
  assets: ZappNavBarAssets;
112
112
  rules: ZappNavBarRules;
113
113
  nav_items: NavBarItem[];
114
+ data: MenuItemDataProperties;
114
115
  } | null {
115
116
  const { activeRiver } = navigator;
116
117
  const { navigations, id: selected, home } = activeRiver; // use useRiver hook instead
@@ -124,7 +125,7 @@ export function getNavigationProps({
124
125
  return null;
125
126
  }
126
127
 
127
- const { styles, assets, nav_items, rules } = navigation;
128
+ const { styles, assets, nav_items, rules, data } = navigation;
128
129
 
129
130
  return {
130
131
  selected,
@@ -134,6 +135,7 @@ export function getNavigationProps({
134
135
  assets: assets as ZappNavBarAssets,
135
136
  nav_items: nav_items as any as NavBarItem[],
136
137
  rules: rules as any as ZappNavBarRules,
138
+ data: data as MenuItemDataProperties,
137
139
  };
138
140
  }
139
141
 
@@ -1,10 +1,10 @@
1
1
  import { toPositiveNumberWithDefault } from "..";
2
2
 
3
- describe("toNumber", () => {
3
+ describe("toPositiveNumberWithDefault", () => {
4
4
  const DEFAULT = 5;
5
5
 
6
6
  it("return number if input is positive number", () => {
7
- const inputs = [0, 1, 2, 100, 200.12, Infinity];
7
+ const inputs = [1, 2, 100, 200.12, Infinity];
8
8
  expect.assertions(inputs.length);
9
9
 
10
10
  inputs.forEach((input) => {
@@ -14,7 +14,7 @@ describe("toNumber", () => {
14
14
  });
15
15
 
16
16
  it("return number if input is positive number as string", () => {
17
- const inputs = ["0", "1", "2", "100", "200.12"];
17
+ const inputs = ["1", "2", "100", "200.12"];
18
18
  expect.assertions(inputs.length);
19
19
 
20
20
  inputs.forEach((input) => {
@@ -63,4 +63,24 @@ describe("toNumber", () => {
63
63
  expect(output).toBe(DEFAULT);
64
64
  });
65
65
  });
66
+
67
+ it("return default if input is zero number", () => {
68
+ const inputs = [0, -0];
69
+ expect.assertions(inputs.length);
70
+
71
+ inputs.forEach((input) => {
72
+ const output = toPositiveNumberWithDefault(DEFAULT, input);
73
+ expect(output).toBe(DEFAULT);
74
+ });
75
+ });
76
+
77
+ it("return default if input is zero string", () => {
78
+ const inputs = ["0", "-0"];
79
+ expect.assertions(inputs.length);
80
+
81
+ inputs.forEach((input) => {
82
+ const output = toPositiveNumberWithDefault(DEFAULT, input);
83
+ expect(output).toBe(DEFAULT);
84
+ });
85
+ });
66
86
  });
@@ -83,6 +83,10 @@ export const toPositiveNumberWithDefault = (
83
83
  return defaultValue;
84
84
  }
85
85
 
86
+ if (isZero(possibleNumber)) {
87
+ return defaultValue;
88
+ }
89
+
86
90
  return possibleNumber;
87
91
  };
88
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "13.0.0-alpha.2807014304",
3
+ "version": "13.0.0-alpha.2957047383",
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": "13.0.0-alpha.2807014304",
30
+ "@applicaster/applicaster-types": "13.0.0-alpha.2957047383",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -2557,6 +2557,12 @@ function getPlayerConfiguration({ platform }) {
2557
2557
  multiple: false,
2558
2558
  label_tooltip: "Choose the default aspect ratio for the artwork",
2559
2559
  },
2560
+ {
2561
+ key: "audio_player_artwork_border_radius",
2562
+ label: "Audio Player Artwork Corner Radius",
2563
+ type: "number_input",
2564
+ initial_value: 16,
2565
+ },
2560
2566
  ]
2561
2567
  )
2562
2568
  );
@@ -5,6 +5,7 @@ import * as R from "ramda";
5
5
  import { useNavigation } from "../navigation";
6
6
  import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
7
7
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
8
+ import { dismissModal, openModal } from "../../modalState";
8
9
 
9
10
  const GOOGLE_ADS_IDENTIFIER = "google-admob-ads-qb";
10
11
 
@@ -14,9 +15,8 @@ const ModalContainer = platformSelect({
14
15
  });
15
16
 
16
17
  export function useAdvertisingInterstitial() {
17
- const navigator = useNavigation();
18
+ const { screenData, currentRoute } = useNavigation();
18
19
  const { plugins } = usePickFromState();
19
- const { screenData } = navigator;
20
20
 
21
21
  return useEffect(() => {
22
22
  // TODD: typing problem: fix any type
@@ -26,7 +26,7 @@ export function useAdvertisingInterstitial() {
26
26
  plugins
27
27
  );
28
28
 
29
- navigator.openModal({
29
+ openModal({
30
30
  item: {},
31
31
  options: {
32
32
  ModalContainer,
@@ -36,11 +36,11 @@ export function useAdvertisingInterstitial() {
36
36
  screenType: "hooks",
37
37
  screenData: {
38
38
  payload: screenData,
39
- callback: navigator.dismissModal,
39
+ callback: dismissModal,
40
40
  hookPlugin: GoogleAdsPlugin,
41
41
  },
42
42
  },
43
43
  });
44
44
  }
45
- }, [navigator.currentRoute, screenData?.id]);
45
+ }, [currentRoute, screenData?.id]);
46
46
  }
@@ -5,7 +5,7 @@ import { loadPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
5
5
  import { useZappPipesFeed } from "@applicaster/zapp-react-native-redux/hooks";
6
6
 
7
7
  import { reactHooksLogger } from "../logger";
8
- import { useIsInitialRender, shouldDispatchData } from "../utils";
8
+ import { shouldDispatchData, useIsInitialRender } from "../utils";
9
9
  import { useInflatedUrl } from "./useInflatedUrl";
10
10
  import { useRoute } from "../navigation";
11
11
 
@@ -20,6 +20,7 @@ type Props = {
20
20
  silentRefresh?: boolean;
21
21
  method?: "get" | "post";
22
22
  bodyParams?: Record<string, unknown>;
23
+ skipLoading?: boolean;
23
24
  };
24
25
  };
25
26
 
@@ -55,7 +56,9 @@ export const useFeedLoader = ({
55
56
 
56
57
  const currentFeed = useZappPipesFeed(callableFeedUrl);
57
58
 
58
- const riverId = screenData?.id;
59
+ const riverId =
60
+ (screenData as LegacyNavigationScreenData)?.targetScreen?.id ??
61
+ screenData?.id;
59
62
 
60
63
  const reloadData = React.useCallback<ReloadDataFunction>(
61
64
  (silentRefresh = true, callback) => {
@@ -93,7 +96,7 @@ export const useFeedLoader = ({
93
96
  if (
94
97
  shouldDispatchData(callableFeedUrl, currentFeed, pipesOptions.clearCache)
95
98
  ) {
96
- if (callableFeedUrl) {
99
+ if (callableFeedUrl && !pipesOptions.skipLoading) {
97
100
  dispatch(
98
101
  loadPipesData(callableFeedUrl, {
99
102
  ...pipesOptions,
@@ -101,26 +104,33 @@ export const useFeedLoader = ({
101
104
  riverId,
102
105
  })
103
106
  );
104
- } else {
107
+ } else if (!callableFeedUrl) {
105
108
  logger.info({
106
109
  message:
107
110
  "Feed is not defined or not inflated, skipping loading. \nMake sure you have correct feedUrl and mapping.",
108
111
  jsOnly: true,
109
112
  });
113
+ } else {
114
+ logger.info({
115
+ message:
116
+ "Loading skipped as per pipesOptions.skipLoading configuration.",
117
+ data: { url: callableFeedUrl },
118
+ jsOnly: true,
119
+ });
110
120
  }
111
121
  } else {
112
122
  logger.info({
113
123
  message:
114
- "Feed already exist, skipping loading. \nCall reloadData() if you want to refresh data.",
124
+ "Feed already exists, skipping loading. \nCall reloadData() if you want to refresh data.",
115
125
  data: { url: callableFeedUrl },
116
126
  jsOnly: true,
117
127
  });
118
128
  }
119
129
  }, []);
120
130
 
121
- // Reload feed when feedUrl changes
131
+ // Reload feed when feedUrl changes, unless skipLoading is true
122
132
  useEffect(() => {
123
- if (!isInitialRender && callableFeedUrl) {
133
+ if (!isInitialRender && callableFeedUrl && !pipesOptions.skipLoading) {
124
134
  dispatch(loadPipesData(callableFeedUrl, { ...pipesOptions, riverId }));
125
135
  }
126
136
  }, [callableFeedUrl]);
@@ -156,7 +166,6 @@ export const useFeedLoader = ({
156
166
  currentFeed?.data,
157
167
  currentFeed?.error,
158
168
  currentFeed?.loading,
159
-
160
169
  reloadData,
161
170
  loadNext,
162
171
  ]);
@@ -0,0 +1,39 @@
1
+ import { reactHooksLogger } from "../../logger";
2
+ import { useZappHooksModalState } from "@applicaster/zapp-react-dom-ui-components/Components/ZappHooksModal/hooks/useZappHooksModalState";
3
+
4
+ const logger = reactHooksLogger.addSubsystem("useHookModalScreenData");
5
+
6
+ type VariousScreenData = LegacyNavigationScreenData | ZappRiver | ZappEntry;
7
+
8
+ export const useHookModalScreenData = (): VariousScreenData | undefined => {
9
+ const hookModalState = useZappHooksModalState();
10
+
11
+ if (!hookModalState.state?.screenData) {
12
+ return;
13
+ }
14
+
15
+ try {
16
+ const screenData = hookModalState.state.screenData?.payload;
17
+ const hookPlugin = hookModalState.state.screenData.hookPlugin;
18
+
19
+ if (hookPlugin?.screen_id) {
20
+ return {
21
+ ...screenData,
22
+ targetScreen: hookPlugin,
23
+ };
24
+ }
25
+
26
+ return hookModalState.state.screenData?.payload;
27
+ } catch (error) {
28
+ logger.error({
29
+ message: "Hook modal screen data creation failed",
30
+ data: {
31
+ error,
32
+ hookModalState,
33
+ },
34
+ jsOnly: true,
35
+ });
36
+
37
+ return hookModalState.state.screenData?.payload;
38
+ }
39
+ };
@@ -7,12 +7,11 @@ import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
7
7
 
8
8
  import { legacyScreenData } from "@applicaster/quick-brick-core/App/NavigationProvider/utils";
9
9
 
10
- import { useZappHooksModalState } from "@applicaster/zapp-react-dom-ui-components/Components/ZappHooksModal/hooks/useZappHooksModalState";
11
-
12
10
  import { getTargetScreenData } from "../screen/useTargetScreenData";
11
+ import { useHookModalScreenData } from "../hookModal/hooks/useHookModalScreenData";
13
12
 
14
13
  import { useNavigation } from "./useNavigation";
15
- import { useModalStore } from "../../modalState";
14
+ import { useModalStoreState } from "../../modalState";
16
15
  import { ScreenDataContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenDataContext";
17
16
  import { usePathname } from "./usePathname";
18
17
 
@@ -44,12 +43,11 @@ export const useRoute = (): {
44
43
  "contentTypes",
45
44
  ]);
46
45
 
47
- const { modalState } = useModalStore();
48
- const hookModalState = useZappHooksModalState();
46
+ const modalState = useModalStoreState();
49
47
 
50
48
  const modalScreenData = modalState.screen;
51
49
 
52
- const hookModalScreenData = hookModalState.state.screenData?.payload;
50
+ const hookModalScreenData = useHookModalScreenData();
53
51
 
54
52
  const videoModalScreenData =
55
53
  navigator?.videoModalState?.item &&
@@ -0,0 +1,30 @@
1
+ ### ramda replacement utils
2
+
3
+ See contribution guide below
4
+
5
+ ## Reference
6
+
7
+ Prefer non-fp variant as it more performant - if refactoring assumes you need more efforts rewrite ramda sample to lodash and it can be easily replaced with lodash-fp - use lodash-fp.
8
+
9
+ ### Ramda alternatives:
10
+
11
+ Custom
12
+ - `splitEvery` => `chunk`
13
+ - `times` => `times`
14
+
15
+ From `lodash`
16
+ - `clone` => `clone`
17
+ - `flatten` => `flatten`
18
+ - `drop` => `drop`
19
+ - `length` => `size`
20
+
21
+
22
+ ## Contribution
23
+
24
+ To add new helper function as ramda replacement
25
+
26
+ 1. Search for replacement ( you can use lodash or pure JS solution), arguments should match with Ramda definition (for easier transition).
27
+ 2. Add new file if needed, add test, update exports in index file. 2.1. Make sure it's well-typed.
28
+ 3. If the function can-be reexported from other 3rd party lib - update index.ts to re-export it.
29
+ 4. Make sure README is updated and contains reference for newly added function.
30
+ 5. You can use AI's help. Use query like 'lodash & lodash/fp equivalent of Ramda's R.clone function' to get an options you can use.
package/utils/chunk.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { chunk as _chunk } from "lodash";
2
+
3
+ export const chunk = <T>(size: number, array: T[]): T[][] => {
4
+ return _chunk(array, size);
5
+ };
package/utils/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { chunk } from "./chunk";
2
+
3
+ export { times } from "./times";
4
+
5
+ export { cloneDeep as clone, flatten, drop, size } from "lodash";
package/utils/times.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { times as _times } from "lodash";
2
+
3
+ export const times = <T>(iteratee: (index: number) => T, n: number) => {
4
+ return _times(n, iteratee);
5
+ };
@@ -1,6 +1,12 @@
1
1
  import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
2
2
  import { isNilOrEmpty } from "../reactUtils/helpers";
3
3
  import { parseJsonIfNeeded } from "@applicaster/zapp-react-native-utils/functionUtils";
4
+ import {
5
+ StorageOwnedValues,
6
+ NamespaceValues,
7
+ StorageValuesToRemove,
8
+ StorageValuesToAdd,
9
+ } from "./types";
4
10
 
5
11
  const defaultOwnedKey = "owned_keys";
6
12
  const DEFAULT_NAMESPACE = "applicaster.v2";
@@ -89,11 +89,15 @@ const runInBackground = async ({
89
89
  const shouldPresent = await hookPresentPolicy.shouldPresentScreen();
90
90
 
91
91
  if (!shouldPresent) {
92
+ const isRefreshed = await checkUserAuthenticated();
93
+
92
94
  log_info(
93
- "runInBackground: Background mode finished, hook plugin will be skipped"
95
+ `runInBackground: Background mode finished, ${isRefreshed ? "finish hook with completion true" : "refresh failed will presentUI"}`
94
96
  );
95
97
 
96
- await checkUserAuthenticated();
98
+ isRefreshed
99
+ ? callback({ success: isRefreshed, payload: item })
100
+ : presentUI();
97
101
 
98
102
  return;
99
103
  }
@@ -0,0 +1,11 @@
1
+ export type StorageValuesToAdd = Record<string, NamespaceValues>;
2
+
3
+ export type StorageValuesToRemove = {
4
+ string?: string[];
5
+ };
6
+
7
+ export type StorageOwnedValues = {
8
+ string?: string[];
9
+ };
10
+
11
+ export type NamespaceValues = Record<string, string>;