@applicaster/zapp-react-native-utils 16.0.0-alpha.1929433645 → 16.0.0-alpha.2105365027

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 (46) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +139 -5
  3. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
  4. package/actionsExecutor/actions/navigateToScreen.ts +29 -45
  5. package/actionsExecutor/actions/openBottomSheet.ts +1 -0
  6. package/appUtils/playerManager/__tests__/playerNative.test.ts +76 -0
  7. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  8. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +97 -0
  9. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  10. package/appUtils/playerManager/index.ts +7 -0
  11. package/appUtils/playerManager/player.ts +1 -0
  12. package/appUtils/playerManager/playerNative.ts +22 -0
  13. package/appUtils/playerManager/playerNativeCommand.ts +2 -6
  14. package/appUtils/playerManager/playerToolbox.ts +157 -0
  15. package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
  16. package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
  17. package/arrayUtils/index.ts +26 -43
  18. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  19. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  20. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  21. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  22. package/cellUtils/index.ts +74 -19
  23. package/configurationUtils/__tests__/modalBlocksParser.test.ts +123 -0
  24. package/configurationUtils/index.ts +77 -73
  25. package/configurationUtils/modalBlocksParser.ts +38 -0
  26. package/focusManager/FocusManager.ts +26 -0
  27. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  28. package/localizationUtils/index.ts +12 -6
  29. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  30. package/logger/xrayToolbox.ts +67 -0
  31. package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
  32. package/manifestUtils/keys.js +6 -1
  33. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  34. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  35. package/manifestUtils/mobileAction/groups/defaults.js +2 -1
  36. package/navigationUtils/xrayNavigator.ts +12 -0
  37. package/package.json +2 -2
  38. package/perfTracker.ts +203 -0
  39. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  40. package/reactHooks/connection/index.tsx +8 -5
  41. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  42. package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
  43. package/reactHooks/usePluginConfiguration.ts +84 -46
  44. package/riverComponetsMeasurementProvider/index.tsx +8 -1
  45. package/uiActionsRegistrator/__tests__/subscribe.test.ts +26 -0
  46. package/uiActionsRegistrator/registry.ts +16 -0
@@ -3,46 +3,91 @@ import React from "react";
3
3
  import { getDatasourceUrl } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/getDatasourceUrl";
4
4
  import { usePipesContexts } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/usePipesContexts";
5
5
  import { markPipesDataStale } from "@applicaster/zapp-react-native-redux/ZappPipes";
6
-
7
- import { useRoute } from "../navigation";
8
6
  import { useAppDispatch } from "@applicaster/zapp-react-native-redux";
7
+ import { isTrue } from "../../booleanUtils";
8
+ import { refreshCoordinator } from "../../refreshUtils/RefreshCoordinator";
9
+ import { useNavigation, useRoute } from "../navigation";
10
+ import { usePrevious } from "../utils";
11
+
12
+ const getRefreshableComponents = (components: ZappUIComponent[]) =>
13
+ components?.filter(
14
+ (c) => isTrue(c?.rules?.clear_cache_on_reload) && c?.data?.source
15
+ ) || [];
9
16
 
10
17
  /**
11
18
  * Mark a river's `clear_cache_on_reload` feeds as stale when the screen is
12
19
  * unmounted. The cached data is kept (so a screen sharing the same feed does
13
20
  * not lose it mid-mount) and revalidated on next access.
21
+ *
22
+ * Also triggers refresh for mounted components and marks feeds stale when
23
+ * returning back to this screen from a deeper route in the Transitioner
24
+ * (where the screen was kept mounted in the background).
25
+ *
14
26
  * @param {string} riverId screen id
15
27
  * @param {Array} riverComponents list of UI components
16
28
  */
17
- export const useMarkPipesDataStale = (riverId, riverComponents) => {
29
+ export const useMarkPipesDataStale = (
30
+ riverId: string,
31
+ riverComponents: ZappUIComponent[]
32
+ ) => {
18
33
  const dispatch = useAppDispatch();
19
34
  const { screenData, pathname } = useRoute();
20
35
  const pipesContexts = usePipesContexts(riverId, pathname);
36
+ const { currentRoute } = useNavigation();
37
+ const prevRoute = usePrevious(currentRoute);
21
38
 
22
- React.useEffect(() => {
23
- return () => {
24
- riverComponents.forEach((component) => {
25
- if (
26
- component?.rules?.clear_cache_on_reload &&
27
- component?.data?.source &&
28
- // We don't want to refresh cache when closing/minimising videoModal
29
- !pathname.includes("videoModal")
30
- ) {
31
- const url = getDatasourceUrl(
32
- {
33
- screenData,
34
- ...pipesContexts,
35
- },
36
- component
37
- );
38
-
39
- if (url) {
40
- dispatch(markPipesDataStale(url, { riverId }));
41
- }
39
+ const isBackToScreen = Boolean(
40
+ prevRoute && prevRoute !== currentRoute && currentRoute === pathname
41
+ );
42
+
43
+ const invalidateComponents = React.useCallback(
44
+ (components: ZappUIComponent[]) => {
45
+ components.forEach((component) => {
46
+ const url = getDatasourceUrl(
47
+ { screenData, ...pipesContexts },
48
+ component
49
+ );
50
+
51
+ if (url) {
52
+ dispatch(markPipesDataStale(url, { riverId }));
42
53
  }
43
54
  });
55
+ },
56
+ [dispatch, screenData, pipesContexts, riverId]
57
+ );
58
+
59
+ // Stale invalidation on unmount (existing behavior)
60
+ /* eslint-disable @wogns3623/better-exhaustive-deps/exhaustive-deps */
61
+ React.useEffect(() => {
62
+ return () => {
63
+ if (pathname?.includes("videoModal")) return;
64
+
65
+ invalidateComponents(getRefreshableComponents(riverComponents));
44
66
  };
45
67
  }, []);
68
+ /* eslint-enable @wogns3623/better-exhaustive-deps/exhaustive-deps */
69
+
70
+ // Reload when returning back to this screen from another screen
71
+ React.useEffect(() => {
72
+ if (!isBackToScreen || pathname?.includes("videoModal")) {
73
+ return;
74
+ }
75
+
76
+ const refreshable = getRefreshableComponents(riverComponents);
77
+
78
+ if (refreshable.length === 0) {
79
+ return;
80
+ }
81
+
82
+ invalidateComponents(refreshable);
83
+ refreshCoordinator.triggerRefresh(refreshable, riverId);
84
+ }, [
85
+ isBackToScreen,
86
+ pathname,
87
+ riverComponents,
88
+ riverId,
89
+ invalidateComponents,
90
+ ]);
46
91
  };
47
92
 
48
93
  /**
@@ -1,4 +1,3 @@
1
- import * as R from "ramda";
2
1
  import { useMemo } from "react";
3
2
 
4
3
  import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
@@ -6,7 +5,8 @@ import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
6
5
  // importing this way prevents typescript packages importing from this
7
6
  // file to go crazy reporting errors on JS
8
7
  import { populateConfigurationValues } from "@applicaster/zapp-react-native-utils/configurationUtils";
9
- import { useLocalizedStrings } from "../localizationUtils";
8
+ import { getLocalizedStrings, useLocalizedStrings } from "../localizationUtils";
9
+ import { isNil, pathOr } from "../utils";
10
10
 
11
11
  const populateFields = (
12
12
  data,
@@ -14,16 +14,72 @@ const populateFields = (
14
14
  pathToValues: string | string[] = "configuration",
15
15
  skipDefaults = false
16
16
  ) =>
17
- R.compose(
18
- (config) =>
19
- populateConfigurationValues(manifestFields, config, skipDefaults),
20
- R.pathOr({}, R.unless(Array.isArray, Array)(pathToValues))
21
- )(data);
17
+ populateConfigurationValues(
18
+ manifestFields,
19
+ pathOr({}, pathToValues, data),
20
+ skipDefaults
21
+ );
22
22
 
23
23
  export function parsePluginConfiguration(plugin, manifest) {
24
24
  return populateFields(plugin, manifest.custom_configuration_fields);
25
25
  }
26
26
 
27
+ const getDefaultLocalizations = (manifest) =>
28
+ manifest?.localizations?.fields?.reduce(
29
+ (defaults, { key, initial_value }) => ({
30
+ en: { ...defaults.en, [key]: initial_value },
31
+ }),
32
+ { en: {} }
33
+ ) || {};
34
+
35
+ const isPlainObject = (value) =>
36
+ Object.prototype.toString.call(value) === "[object Object]";
37
+
38
+ // Deep merge where configured values win, except nil ones (defaults fill the gaps)
39
+ export const mergeLocalizations = (configured, defaults) => {
40
+ if (!isPlainObject(configured)) return { ...defaults };
41
+ if (!isPlainObject(defaults)) return { ...configured };
42
+
43
+ // Configured keys first, as R.mergeDeepWith: the first language is the fallback
44
+ return Object.keys({ ...configured, ...defaults }).reduce((merged, key) => {
45
+ const left = configured[key];
46
+ const right = defaults[key];
47
+
48
+ if (!(key in defaults)) {
49
+ merged[key] = left;
50
+ } else if (!(key in configured)) {
51
+ merged[key] = right;
52
+ } else if (isPlainObject(left) && isPlainObject(right)) {
53
+ merged[key] = mergeLocalizations(left, right);
54
+ } else {
55
+ merged[key] = isNil(left) ? right : left;
56
+ }
57
+
58
+ return merged;
59
+ }, {});
60
+ };
61
+
62
+ /**
63
+ * usePluginConfiguration outside React: the same result, read once instead of subscribed.
64
+ */
65
+ export function getPluginConfiguration<T = any, S = Record<string, any>>(
66
+ plugin,
67
+ manifest?: any
68
+ ): { configurationFields: T; localizations: S } {
69
+ return {
70
+ configurationFields: populateFields(
71
+ plugin,
72
+ manifest?.custom_configuration_fields
73
+ ),
74
+ localizations: getLocalizedStrings(
75
+ mergeLocalizations(
76
+ plugin?.configuration?.localizations,
77
+ getDefaultLocalizations(manifest)
78
+ )
79
+ ),
80
+ };
81
+ }
82
+
27
83
  export function usePluginConfiguration<T = any, S = Record<string, any>>(
28
84
  identifier: string,
29
85
  manifest?: any
@@ -31,7 +87,7 @@ export function usePluginConfiguration<T = any, S = Record<string, any>>(
31
87
  const plugins = usePlugins();
32
88
 
33
89
  const plugin = useMemo(
34
- () => R.find(R.propEq("identifier", identifier), plugins),
90
+ () => plugins?.find((item) => item?.identifier === identifier),
35
91
  [plugins, identifier]
36
92
  );
37
93
 
@@ -40,29 +96,20 @@ export function usePluginConfiguration<T = any, S = Record<string, any>>(
40
96
  [plugin, manifest]
41
97
  );
42
98
 
43
- const defaultLocalizations = useMemo(() => {
44
- return (
45
- manifest?.localizations?.fields?.reduce(
46
- (defaults, { key, initial_value }) =>
47
- R.assocPath(["en", key], initial_value, defaults),
48
- { en: {} }
49
- ) || {}
50
- );
51
- }, [manifest]);
52
-
53
- const localizationsParams = useMemo(() => {
54
- return {
55
- localizations: R.mergeDeepWith(
56
- (left, right) => {
57
- if (R.isNil(left)) return right;
99
+ const defaultLocalizations = useMemo(
100
+ () => getDefaultLocalizations(manifest),
101
+ [manifest]
102
+ );
58
103
 
59
- return left;
60
- },
104
+ const localizationsParams = useMemo(
105
+ () => ({
106
+ localizations: mergeLocalizations(
61
107
  plugin?.configuration?.localizations,
62
108
  defaultLocalizations
63
109
  ),
64
- };
65
- }, [plugin?.configuration?.localizations, defaultLocalizations]);
110
+ }),
111
+ [plugin?.configuration?.localizations, defaultLocalizations]
112
+ );
66
113
 
67
114
  const localizations: S = useLocalizedStrings(localizationsParams);
68
115
 
@@ -105,29 +152,20 @@ export function useLayoutConfiguration<
105
152
  "localizations",
106
153
  ];
107
154
 
108
- const defaultLocalizations = useMemo(() => {
109
- return (
110
- manifest?.localizations?.fields?.reduce(
111
- (defaults, { key, initial_value }) =>
112
- R.assocPath(["en", key], initial_value, defaults),
113
- { en: {} }
114
- ) || {}
115
- );
116
- }, [manifest]);
117
-
118
- const localizationsParams = useMemo(() => {
119
- return {
120
- localizations: R.mergeDeepWith(
121
- (left, right) => {
122
- if (R.isNil(left)) return right;
155
+ const defaultLocalizations = useMemo(
156
+ () => getDefaultLocalizations(manifest),
157
+ [manifest]
158
+ );
123
159
 
124
- return left;
125
- },
160
+ const localizationsParams = useMemo(
161
+ () => ({
162
+ localizations: mergeLocalizations(
126
163
  layoutData?.localizations,
127
164
  defaultLocalizations
128
165
  ),
129
- };
130
- }, [layoutData?.localizations, defaultLocalizations]);
166
+ }),
167
+ [layoutData?.localizations, defaultLocalizations]
168
+ );
131
169
 
132
170
  // Always call useLocalizedStrings regardless of whether localizations is in the keys
133
171
  const localizations: L = useLocalizedStrings(localizationsParams);
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { NativeModules, View, StyleSheet } from "react-native";
3
3
  import { getXray } from "@applicaster/zapp-react-native-utils/logger";
4
+ import { countRender, dumpSummary } from "../perfTracker";
4
5
 
5
6
  import { isApplePlatform, isWeb, isTV } from "../reactUtils";
6
7
  import { useRivers } from "../reactHooks";
@@ -34,6 +35,7 @@ type Props = {
34
35
  type ItemProps = {
35
36
  children: React.ReactNode;
36
37
  index: number;
38
+ testID?: string;
37
39
  };
38
40
 
39
41
  type MeasurementContext = {
@@ -76,6 +78,8 @@ export const ScreenLoadingMeasurements = ({
76
78
  (index) => (event) => {
77
79
  const { height } = event.nativeEvent.layout;
78
80
 
81
+ countRender("ScreenLoading_onItemLayout", { index, height });
82
+
79
83
  if (!notified) {
80
84
  // Don't need to set state when event was already sent (optimization)
81
85
  dispatchItemLayout({ payload: { index, height } });
@@ -104,6 +108,8 @@ export const ScreenLoadingMeasurements = ({
104
108
 
105
109
  if (finishLoadingVisible) {
106
110
  if (screenData?.home) {
111
+ dumpSummary("app_home_loaded");
112
+
107
113
  if (isWeb()) {
108
114
  logger.sendAudienceEvent("app_home_loaded", {
109
115
  loading_duration: Date.now() - moduleInitTime,
@@ -153,11 +159,12 @@ export const ScreenLoadingMeasurements = ({
153
159
  export const ScreenLoadingMeasurementsListItemWrapper = ({
154
160
  children,
155
161
  index,
162
+ testID,
156
163
  }: ItemProps) => {
157
164
  const { onLayout } = React.useContext(MeasurementsSettersContext);
158
165
 
159
166
  return (
160
- <View style={styles.container} onLayout={onLayout?.(index)}>
167
+ <View style={styles.container} onLayout={onLayout?.(index)} testID={testID}>
161
168
  {children}
162
169
  </View>
163
170
  );
@@ -105,3 +105,29 @@ describe("isActionAvailableFor", () => {
105
105
  expect(isActionAvailableFor(undefined, entry)).toBe(true);
106
106
  });
107
107
  });
108
+
109
+ describe("uiActionsRegistry.notifyProviderChanged", () => {
110
+ it("notifies subscribers for a registered provider", () => {
111
+ const unregister = uiActionsRegistry.registerAction("live-action", value);
112
+ const listener = jest.fn();
113
+ const unsubscribe = uiActionsRegistry.subscribe(listener);
114
+
115
+ uiActionsRegistry.notifyProviderChanged("live-action");
116
+
117
+ expect(listener).toHaveBeenCalledTimes(1);
118
+
119
+ unsubscribe();
120
+ unregister();
121
+ });
122
+
123
+ it("ignores names that are not registered", () => {
124
+ const listener = jest.fn();
125
+ const unsubscribe = uiActionsRegistry.subscribe(listener);
126
+
127
+ uiActionsRegistry.notifyProviderChanged("unknown-action");
128
+
129
+ expect(listener).not.toHaveBeenCalled();
130
+
131
+ unsubscribe();
132
+ });
133
+ });
@@ -246,6 +246,22 @@ class UIActionsRegistry {
246
246
  this.changeListeners.slice().forEach((listener) => listener());
247
247
  }
248
248
 
249
+ /**
250
+ * Tells consumers that a registered provider now returns different actions.
251
+ *
252
+ * Resolved actions are snapshots: an action whose availability depends on
253
+ * live state (cast devices found, downloads allowed) hands out a new value
254
+ * when that state changes, and consumers that resolved the old one keep
255
+ * asking it until they re-resolve. Registration alone never tells them.
256
+ */
257
+ notifyProviderChanged(name: string): void {
258
+ if (!this.registeredActions[name]) {
259
+ return;
260
+ }
261
+
262
+ this.notifyChanged();
263
+ }
264
+
249
265
  /**
250
266
  * Register a dynamic action provider under a unique `name`.
251
267
  * The provider can return one or more actions and may use the passed `entry`.