@applicaster/zapp-react-native-utils 16.0.0-rc.63 → 16.0.0-rc.65

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.
@@ -0,0 +1,61 @@
1
+ import { decorateFeed } from "../feedDecorator";
2
+
3
+ const preferenceFeed = (selectMode: string) =>
4
+ ({
5
+ id: "agree-component",
6
+ title: "I agree to the terms",
7
+ type: { value: "feed" },
8
+ extensions: {
9
+ role: "preference_editor",
10
+ preference_editor_options: {
11
+ key: "agree",
12
+ scope: "screen",
13
+ select_mode: selectMode,
14
+ },
15
+ },
16
+ entry: [
17
+ {
18
+ id: "I agree to the terms-agree",
19
+ title: "I agree to the terms",
20
+ type: { value: "action" },
21
+ extensions: { tag: "true" },
22
+ },
23
+ ],
24
+ }) as unknown as ZappFeed;
25
+
26
+ describe("decorateFeed", () => {
27
+ it("decorates single select feeds with screenSetVariable", () => {
28
+ const decorated = decorateFeed(preferenceFeed("single"));
29
+
30
+ expect(decorated.extensions.behavior).toEqual(
31
+ expect.objectContaining({
32
+ select_mode: "single",
33
+ current_selection: "@{screen/agree}",
34
+ })
35
+ );
36
+
37
+ expect(decorated.entry[0].extensions.tap_actions.actions[0].type).toBe(
38
+ "screenSetVariable"
39
+ );
40
+ });
41
+
42
+ it("decorates multi select feeds with screenToggleFlag", () => {
43
+ const decorated = decorateFeed(preferenceFeed("multi"));
44
+
45
+ expect(decorated.extensions.behavior.select_mode).toBe("multi");
46
+
47
+ expect(decorated.entry[0].extensions.tap_actions.actions[0].type).toBe(
48
+ "screenToggleFlag"
49
+ );
50
+ });
51
+
52
+ it("leaves a feed without the preference_editor role untouched", () => {
53
+ const feed = preferenceFeed("single");
54
+ feed.extensions.role = undefined;
55
+
56
+ const decorated = decorateFeed(feed);
57
+
58
+ expect(decorated.extensions.behavior).toBeUndefined();
59
+ expect(decorated.entry[0].extensions.tap_actions).toBeUndefined();
60
+ });
61
+ });
@@ -0,0 +1,133 @@
1
+ import { createNavigateToScreenAction } from "../navigateToScreen";
2
+ import { ActionResult } from "../../ActionExecutor";
3
+
4
+ const river = { id: "screen-id", type: "parent-lock" } as unknown as ZappRiver;
5
+
6
+ const rivers = { "screen-id": river } as Record<string, ZappRiver>;
7
+
8
+ const contentTypes = {
9
+ parentLock: { screen_id: "screen-id" },
10
+ } as unknown as ZappContentTypes;
11
+
12
+ function createNavigator() {
13
+ return {
14
+ push: jest.fn(),
15
+ replace: jest.fn(),
16
+ replaceTop: jest.fn(),
17
+ } as unknown as QuickBrickAppNavigator;
18
+ }
19
+
20
+ describe("navigateToScreen navigationOptions", () => {
21
+ it("forwards navigationOptions to push when navigating to a screen", async () => {
22
+ const navigator = createNavigator();
23
+
24
+ const action = {
25
+ type: "navigateToScreen",
26
+ options: {
27
+ typeMapping: "parentLock",
28
+ navigationAction: "push",
29
+ navigationOptions: { pinFlow: "set-pin" },
30
+ },
31
+ };
32
+
33
+ const result = await createNavigateToScreenAction(
34
+ navigator,
35
+ rivers,
36
+ contentTypes
37
+ )(action);
38
+
39
+ expect(navigator.push).toHaveBeenCalledWith(river, {
40
+ pinFlow: "set-pin",
41
+ });
42
+
43
+ expect(result).toBe(ActionResult.Success);
44
+ });
45
+
46
+ it("forwards navigationOptions to replace when navigating to a screen", async () => {
47
+ const navigator = createNavigator();
48
+
49
+ const action = {
50
+ type: "navigateToScreen",
51
+ options: {
52
+ typeMapping: "parentLock",
53
+ navigationOptions: { pinFlow: "change-pin" },
54
+ },
55
+ };
56
+
57
+ await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
58
+
59
+ expect(navigator.replace).toHaveBeenCalledWith(river, {
60
+ pinFlow: "change-pin",
61
+ });
62
+ });
63
+
64
+ it("forwards navigationOptions to push when navigating with an entry", async () => {
65
+ const navigator = createNavigator();
66
+ const entry = { id: "entry-id" } as unknown as ZappEntry;
67
+
68
+ const action = {
69
+ type: "navigateToScreen",
70
+ options: {
71
+ typeMapping: "parentLock",
72
+ navigationAction: "push",
73
+ entry,
74
+ navigationOptions: { pinFlow: "set-pin" },
75
+ },
76
+ };
77
+
78
+ await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
79
+
80
+ expect(navigator.push).toHaveBeenCalledWith(
81
+ { id: "entry-id", type: { value: "parentLock" } },
82
+ { pinFlow: "set-pin" }
83
+ );
84
+ });
85
+
86
+ // Entries cannot go through replace(), which only accepts a river, so the
87
+ // non-push path falls back to replaceTop().
88
+ it("forwards navigationOptions to replaceTop when navigating with an entry", async () => {
89
+ const navigator = createNavigator();
90
+ const entry = { id: "entry-id" } as unknown as ZappEntry;
91
+
92
+ const action = {
93
+ type: "navigateToScreen",
94
+ options: {
95
+ typeMapping: "parentLock",
96
+ entry,
97
+ navigationOptions: { pinFlow: "change-pin" },
98
+ },
99
+ };
100
+
101
+ await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
102
+
103
+ expect(navigator.replaceTop).toHaveBeenCalledWith(
104
+ { id: "entry-id", type: { value: "parentLock" } },
105
+ { pinFlow: "change-pin" }
106
+ );
107
+
108
+ expect(navigator.replace).not.toHaveBeenCalled();
109
+ });
110
+
111
+ // Regression guard: the no-options path must keep working unchanged.
112
+ it("still navigates when no navigationOptions are configured", async () => {
113
+ const navigator = createNavigator();
114
+
115
+ const action = {
116
+ type: "navigateToScreen",
117
+ options: {
118
+ typeMapping: "parentLock",
119
+ navigationAction: "push",
120
+ },
121
+ };
122
+
123
+ const result = await createNavigateToScreenAction(
124
+ navigator,
125
+ rivers,
126
+ contentTypes
127
+ )(action);
128
+
129
+ expect(navigator.push).toHaveBeenCalledTimes(1);
130
+ expect((navigator.push as jest.Mock).mock.calls[0][0]).toBe(river);
131
+ expect(result).toBe(ActionResult.Success);
132
+ });
133
+ });
@@ -25,6 +25,19 @@ export interface NavigateToScreenOptions {
25
25
  * - undefined to navigate to screen without entry
26
26
  */
27
27
  entry?: "@{entry/}" | ZappEntry;
28
+
29
+ /**
30
+ * Arbitrary values passed through to the destination screen.
31
+ *
32
+ * They are stored on the navigation route as `NavigationScreenData.options`
33
+ * and read on the target screen via `useNavigation().data?.options`. Use this
34
+ * to hand per-navigation parameters to a screen instead of routing them
35
+ * through module-level state.
36
+ *
37
+ * Note: `legacyScreenData()` does not carry these values, so they are not
38
+ * visible through `useCurrentScreenData()` / `useRoute()`.
39
+ */
40
+ navigationOptions?: Record<string, unknown>;
28
41
  }
29
42
 
30
43
  /**
@@ -55,6 +68,7 @@ export function createNavigateToScreenAction(
55
68
 
56
69
  const navigationAction = action.options?.navigationAction;
57
70
  const entrySource = action.options?.entry;
71
+ const navigationOptions = action.options?.navigationOptions;
58
72
 
59
73
  const entry = entrySource
60
74
  ? entrySource === "@{entry/}"
@@ -83,10 +97,10 @@ export function createNavigateToScreenAction(
83
97
  };
84
98
 
85
99
  if (navigationAction === "push") {
86
- navigator.push(overriddenEntry);
100
+ navigator.push(overriddenEntry, navigationOptions);
87
101
  } else {
88
102
  // Use replaceTop for entries since replace() only accepts ZappRiver
89
- navigator.replaceTop(overriddenEntry);
103
+ navigator.replaceTop(overriddenEntry, navigationOptions);
90
104
  }
91
105
 
92
106
  return ActionResult.Success;
@@ -113,9 +127,9 @@ export function createNavigateToScreenAction(
113
127
  context?.callback?.({ success: false, error: null, abort: true });
114
128
 
115
129
  if (navigationAction === "push") {
116
- navigator.push(river);
130
+ navigator.push(river, navigationOptions);
117
131
  } else {
118
- navigator.replace(river);
132
+ navigator.replace(river, navigationOptions);
119
133
  }
120
134
 
121
135
  return ActionResult.Success;
@@ -159,13 +159,13 @@ export const decorateFeed = (feed: ZappFeed) => {
159
159
 
160
160
  const decoratedFeed = R.clone(feed);
161
161
 
162
- const isSingleSelect =
163
- (feed.extensions?.preference_editor_options?.select_mode ||
164
- feed.extensions?.behavior?.select_mode) === "single";
162
+ const selectMode =
163
+ feed.extensions?.preference_editor_options?.select_mode ||
164
+ feed.extensions?.behavior?.select_mode;
165
165
 
166
- if (isSingleSelect) {
166
+ if (selectMode === "single") {
167
167
  return makeSingleSelect(feed, key, decoratedFeed);
168
- } else {
169
- return makeMultiSelect(feed, key, decoratedFeed);
170
168
  }
169
+
170
+ return makeMultiSelect(feed, key, decoratedFeed);
171
171
  };
@@ -1 +1,4 @@
1
- export const getUpcomingQueue = async (): Promise<unknown[] | null> => null;
1
+ /** `queue-action` is not supported on this platform. See getUpcomingQueue.ts */
2
+ export const getUpcomingQueue = async (
3
+ _plugin?: QuickBrickPlugin | ZappPlugin
4
+ ): Promise<unknown[] | null> => null;
@@ -1 +1,4 @@
1
- export const getUpcomingQueue = async (): Promise<unknown[] | null> => null;
1
+ /** `queue-action` is not supported on this platform. See getUpcomingQueue.ts */
2
+ export const getUpcomingQueue = async (
3
+ _plugin?: QuickBrickPlugin | ZappPlugin
4
+ ): Promise<unknown[] | null> => null;
@@ -1,5 +1,11 @@
1
- export const getUpcomingQueue = async () => {
2
- const { QueueManager } = require("@applicaster/queue-action/src/Manager");
1
+ export const getUpcomingQueue = async (
2
+ plugin?: QuickBrickPlugin | ZappPlugin
3
+ ): Promise<unknown[] | null> => {
4
+ const QueueManager = (plugin as any)?.module?.QueueManager;
5
+
6
+ if (!QueueManager?.instance) {
7
+ return null;
8
+ }
3
9
 
4
10
  return QueueManager.instance.getUpcomingQueue();
5
11
  };
@@ -1 +1,4 @@
1
- export const getUpcomingQueue = async (): Promise<unknown[] | null> => null;
1
+ /** `queue-action` is not supported on this platform. See getUpcomingQueue.ts */
2
+ export const getUpcomingQueue = async (
3
+ _plugin?: QuickBrickPlugin | ZappPlugin
4
+ ): Promise<unknown[] | null> => null;
@@ -66,11 +66,16 @@ export const retrieveFeedUrl = async (
66
66
  if (!entry) return null;
67
67
 
68
68
  const availablePlugins = plugins || appStore.get("plugins");
69
- const queuePlugin = findPluginByIdentifier("queue-action", availablePlugins);
69
+
70
+ const queuePlugin = findPluginByIdentifier(
71
+ "queue-action",
72
+ availablePlugins,
73
+ true
74
+ );
70
75
 
71
76
  if (queuePlugin) {
72
77
  try {
73
- const upcomingQueue = await getUpcomingQueue();
78
+ const upcomingQueue = await getUpcomingQueue(queuePlugin);
74
79
 
75
80
  if (upcomingQueue && upcomingQueue.length > 0) {
76
81
  const queueItemId = entry?.extensions?.queueItemId || entry?.id;
@@ -65,11 +65,38 @@ export const isEmptyOrNil = (value: any): boolean =>
65
65
  */
66
66
  export const getLabel = (dataKeyValue, customDataKeyValue) => (entry) => {
67
67
  const label = getKeyForLabel(dataKeyValue, customDataKeyValue);
68
- const isPath = label && R.length(label) > 0;
69
- const fallback = isEmptyOrNil(label) ? "" : R.head(label);
68
+ const isPath = label && label.length > 0;
69
+
70
+ // An unresolved key is rendered as-is, because a data key is allowed to be a
71
+ // literal label ("More") rather than a path. Fall back to the whole key
72
+ // instead of its first segment: "extensions.section" reads as an unresolved
73
+ // path, while "extensions" reads as a legitimate label and hides the problem.
74
+ const fallback = isEmptyOrNil(label) ? "" : label.join(".");
70
75
 
71
76
  try {
72
- return isPath ? R.pathOr(fallback, label, entry) : fallback;
77
+ if (!isPath) {
78
+ return fallback;
79
+ }
80
+
81
+ const resolved = label.reduce(
82
+ (value, key) => (value == null ? undefined : value[key]),
83
+ entry
84
+ );
85
+
86
+ if (isNotNil(resolved)) {
87
+ return resolved;
88
+ }
89
+
90
+ // A multi-segment key is unambiguously a path, so failing to resolve it is
91
+ // a misconfiguration rather than someone typing a literal label.
92
+ if (label.length > 1) {
93
+ cellUtilsLogger.warning({
94
+ message: `getLabel: data key "${fallback}" was not found on the entry, rendering the key itself`,
95
+ data: { dataKeyValue, customDataKeyValue, entryId: entry?.id },
96
+ });
97
+ }
98
+
99
+ return fallback;
73
100
  } catch (error) {
74
101
  cellUtilsLogger.warning({
75
102
  data: { error, dataKeyValue, customDataKeyValue },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "16.0.0-rc.63",
3
+ "version": "16.0.0-rc.65",
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-rc.63",
30
+ "@applicaster/applicaster-types": "16.0.0-rc.65",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",