@applicaster/zapp-react-native-utils 12.0.16 → 12.0.17-rc.0

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.
@@ -17,9 +17,10 @@ class ActionExecutor {
17
17
 
18
18
  handleAction = async (
19
19
  action: ActionType,
20
- entry: ZappEntry = null
20
+ context?: Record<string, any>
21
21
  ): Promise<ActionResult> => {
22
22
  const handler = this.registeredActions[action.type];
23
+ const entry = context?.entry as ZappEntry;
23
24
 
24
25
  if (!handler) {
25
26
  log_error(
@@ -38,11 +39,11 @@ class ActionExecutor {
38
39
  );
39
40
 
40
41
  try {
41
- return handler(action, entry);
42
+ return handler(action, context);
42
43
  } catch (error) {
43
44
  log_error(
44
45
  `handleAction: error handling action type: ${action.type}, message: ${error.message}`,
45
- { action, entry, error }
46
+ { action, context, error }
46
47
  );
47
48
 
48
49
  throw error;
@@ -51,11 +52,11 @@ class ActionExecutor {
51
52
 
52
53
  handleActions = async (
53
54
  actionTypes: ActionType[],
54
- entry: ZappEntry = null
55
+ context?: Record<string, any>
55
56
  ): Promise<ActionResult> => {
56
57
  for (const action of actionTypes) {
57
58
  // TODO: Handle action result error
58
- const result = await this.handleAction(action, entry);
59
+ const result = await this.handleAction(action, context);
59
60
 
60
61
  if (result === ActionResult.Cancel) {
61
62
  log_info(`handleActions: action: ${action.type} canceled`);
@@ -67,7 +68,10 @@ class ActionExecutor {
67
68
  return ActionResult.Success;
68
69
  };
69
70
 
70
- handleEntryActions = async (entry: ZappEntry): Promise<ActionResult> => {
71
+ handleEntryActions = async (
72
+ entry: ZappEntry,
73
+ context?: Record<string, any>
74
+ ): Promise<ActionResult> => {
71
75
  const tapActions = entry.extensions?.tap_actions?.actions;
72
76
 
73
77
  if (!tapActions) {
@@ -77,7 +81,7 @@ class ActionExecutor {
77
81
  if (!Array.isArray(tapActions)) {
78
82
  log_error(
79
83
  `handleEntryActions: tapActions is not an array for id: ${entry.id} title: ${entry.title}`,
80
- { entry }
84
+ { context, entry }
81
85
  );
82
86
 
83
87
  return;
@@ -87,7 +91,7 @@ class ActionExecutor {
87
91
  return;
88
92
  }
89
93
 
90
- await this.handleActions(tapActions, entry);
94
+ await this.handleActions(tapActions, { ...context, entry });
91
95
  };
92
96
 
93
97
  unregisterAction = (actionType: string) => {
@@ -96,7 +100,10 @@ class ActionExecutor {
96
100
 
97
101
  registerAction = (
98
102
  type: string,
99
- handler: (action: ActionType, entry?: ZappEntry) => Promise<ActionResult>
103
+ handler: (
104
+ action: ActionType,
105
+ context?: Record<string, any>
106
+ ) => Promise<ActionResult>
100
107
  ) => {
101
108
  if (this.registeredActions[type]) {
102
109
  log_error(`registerAction: action type: ${type} already registered`);
@@ -1,5 +1,5 @@
1
1
  /// <reference types="@applicaster/applicaster-types" />
2
-
2
+ import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
3
3
  import React, { createContext, useMemo } from "react";
4
4
  import {
5
5
  actionExecutor as _actionExecutor,
@@ -12,6 +12,14 @@ import { showConfirmationDialog } from "../alertUtils";
12
12
  import { createCloudEvent, sendCloudEvent } from "../cloudEventsUtils";
13
13
  import { createLogger } from "../logger";
14
14
  import { StorageMultiSelectProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/StorageMultiSelectProvider";
15
+ import { ACTIVE_LAYOUT_ID_STORAGE_KEY } from "@applicaster/quick-brick-core/App/remoteContextReloader/consts";
16
+ import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
17
+ import { loadPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
18
+ import {
19
+ EntryResolver,
20
+ resolveObjectValues,
21
+ } from "../appUtils/contextKeysManager/contextResolver";
22
+ import { get } from "lodash";
15
23
 
16
24
  const { log_error, log_info } = createLogger({
17
25
  subsystem: "ActionExecutorContext",
@@ -21,15 +29,24 @@ const { log_error, log_info } = createLogger({
21
29
  type ActionExecutorContextType = {
22
30
  registerAction: (
23
31
  type: string,
24
- handler: (action: ActionType, entry: ZappEntry) => Promise<ActionResult>
32
+ handler: (
33
+ action: ActionType,
34
+ context?: Record<string, any>
35
+ ) => Promise<ActionResult>
25
36
  ) => void;
26
37
  unregisterAction: (type: string) => void;
27
- handleAction: (action: ActionType, entry: ZappEntry) => Promise<ActionResult>;
38
+ handleAction: (
39
+ action: ActionType,
40
+ context?: Record<string, any>
41
+ ) => Promise<ActionResult>;
28
42
  handleActions: (
29
43
  actions: ActionType[],
30
- entry: ZappEntry
44
+ context: Record<string, any>
45
+ ) => Promise<ActionResult>;
46
+ handleEntryActions: (
47
+ entry: ZappEntry,
48
+ context?: Record<string, any>
31
49
  ) => Promise<ActionResult>;
32
- handleEntryActions: (entry: ZappEntry) => Promise<ActionResult>;
33
50
  };
34
51
  type Props = {
35
52
  children: React.ReactNode;
@@ -44,6 +61,44 @@ const prepareDefaultActions = (actionExecutor) => {
44
61
  return ActionResult.Success;
45
62
  });
46
63
 
64
+ actionExecutor.registerAction(
65
+ "refreshComponent",
66
+ async (_action, context) => {
67
+ const dispatch = appStore.getDispatch();
68
+
69
+ log_info("handleAction: refreshComponent event");
70
+
71
+ const dataSource = context?.component?.data?.source;
72
+
73
+ // TODO: Later we can pass component ids to do refresh instead of current one
74
+ // TODO: In theory we should wait callback to complete, before completing the action, but now it's not needed
75
+ // TODO: handle focused item removal
76
+ dispatch(
77
+ loadPipesData(dataSource, {
78
+ silentRefresh: false,
79
+ clearCache: true,
80
+ callback: (_data) => {},
81
+ riverId: context?.screenData?.id,
82
+ })
83
+ );
84
+
85
+ return ActionResult.Success;
86
+ }
87
+ );
88
+
89
+ actionExecutor.registerAction("switchLayout", async (action) => {
90
+ log_info("handleAction: switchLayout event");
91
+
92
+ await localStorage.setItem(
93
+ ACTIVE_LAYOUT_ID_STORAGE_KEY,
94
+ action.options.layoutId
95
+ );
96
+
97
+ QuickBrickManager.sendQuickBrickEvent(QUICK_BRICK_EVENTS.FORCE_APP_RELOAD);
98
+
99
+ return ActionResult.Success;
100
+ });
101
+
47
102
  actionExecutor.registerAction("appRestart", async () => {
48
103
  log_info(`handleAction: ${QUICK_BRICK_EVENTS.FORCE_APP_RELOAD} event`);
49
104
 
@@ -66,13 +121,21 @@ const prepareDefaultActions = (actionExecutor) => {
66
121
  return confirmationPromise;
67
122
  });
68
123
 
69
- actionExecutor.registerAction("sendCloudEvent", async (action, entry) => {
124
+ actionExecutor.registerAction("sendCloudEvent", async (action, context) => {
70
125
  try {
71
126
  const options = action.options;
72
127
 
128
+ const entry = context?.entry || {};
129
+ const entryResolver = new EntryResolver(entry);
130
+
131
+ const data =
132
+ options?.data && options.inflateData
133
+ ? await resolveObjectValues(options.data, { entry: entryResolver })
134
+ : options?.data || entry;
135
+
73
136
  const cloudEvent = await createCloudEvent({
74
137
  type: options.type || "com.applicaster.selector.action.v1",
75
- data: options.data || entry,
138
+ data,
76
139
  subject: options.subject || entry?.id,
77
140
  });
78
141
 
@@ -103,9 +166,33 @@ const prepareDefaultActions = (actionExecutor) => {
103
166
 
104
167
  actionExecutor.registerAction(
105
168
  "localStorageToggleFlag",
106
- async (action: ActionType, entry?: ZappEntry): Promise<ActionResult> => {
169
+ async (
170
+ action: ActionType,
171
+ context?: Record<string, any>
172
+ ): Promise<ActionResult> => {
173
+ if (!context) {
174
+ log_error(
175
+ "handleAction: localStorageToggleFlag action missing context"
176
+ );
177
+
178
+ return ActionResult.Error;
179
+ }
180
+
181
+ const entry = context?.entry as ZappEntry;
182
+
183
+ if (!entry) {
184
+ log_error(
185
+ "handleAction: localStorageToggleFlag action missing entry. Entry is required to get the tag."
186
+ );
187
+
188
+ return ActionResult.Error;
189
+ }
190
+
191
+ const tag = action.options?.selector
192
+ ? get(entry, action.options.selector)
193
+ : (entry.extensions?.tag ?? entry.id);
194
+
107
195
  const keyNamespace = action.options?.key;
108
- const tag = entry?.extensions?.tag;
109
196
 
110
197
  if (keyNamespace && tag) {
111
198
  const multiSelectProvider =
@@ -0,0 +1,148 @@
1
+ import { Alert } from "react-native";
2
+ import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
3
+ import { confirmationDialogStore } from "@applicaster/zapp-react-native-ui-components/Contexts/ConfirmDialogState";
4
+
5
+ // #region "Alert Dialog"
6
+
7
+ const globalAny: any = global;
8
+
9
+ const showWebAlertDialog = ({ message, completion }: AlertWebDialog) => {
10
+ globalAny?.alert?.(message);
11
+ completion?.();
12
+ };
13
+
14
+ const showNativeAlertDialog = ({
15
+ title,
16
+ message,
17
+ okButtonText,
18
+ completion,
19
+ }: AlertDialogType) => {
20
+ Alert.alert(title, message, [
21
+ {
22
+ text: okButtonText,
23
+ onPress: completion,
24
+ },
25
+ ]);
26
+ };
27
+
28
+ export const showAlertDialog = ({
29
+ title,
30
+ message,
31
+ okButtonText,
32
+ completion,
33
+ }: AlertDialogType) => {
34
+ if (isWeb()) {
35
+ showWebAlertDialog({ message, completion });
36
+ } else {
37
+ showNativeAlertDialog({
38
+ title,
39
+ message,
40
+ okButtonText,
41
+ completion,
42
+ });
43
+ }
44
+ };
45
+
46
+ // #endregion "Alert Dialog"
47
+
48
+ // #region "Confirmation Dialog"
49
+
50
+ const showWebDialog = ({
51
+ message,
52
+ confirmCompletion,
53
+ cancelCompletion,
54
+ }: WebConfirmationDialog) => {
55
+ const isConfirmed = globalAny?.confirm?.(message);
56
+
57
+ if (isConfirmed) {
58
+ confirmCompletion?.();
59
+ } else {
60
+ cancelCompletion?.();
61
+ }
62
+ };
63
+
64
+ const showNativeDialog = ({
65
+ title,
66
+ message,
67
+ okButtonText,
68
+ cancelButtonText,
69
+ confirmCompletion,
70
+ cancelCompletion,
71
+ }: ConfirmationDialog) => {
72
+ Alert.alert(title, message, [
73
+ {
74
+ text: cancelButtonText,
75
+ onPress: cancelCompletion,
76
+ },
77
+ {
78
+ text: okButtonText,
79
+ style: "destructive",
80
+ onPress: confirmCompletion,
81
+ },
82
+ ]);
83
+ };
84
+
85
+ const handleCustomDialog = (data) => {
86
+ const {
87
+ showDialog,
88
+ hideDialog,
89
+ setConfirmAction,
90
+ setCancelAction,
91
+ setMessages,
92
+ } = confirmationDialogStore.getState();
93
+
94
+ setMessages(data.messages);
95
+
96
+ if (data?.cancelCompletion) {
97
+ setCancelAction(() => {
98
+ data.cancelCompletion();
99
+ hideDialog();
100
+ });
101
+ }
102
+
103
+ if (data?.confirmCompletion) {
104
+ setConfirmAction(() => {
105
+ data.confirmCompletion();
106
+ hideDialog();
107
+ });
108
+ }
109
+
110
+ showDialog();
111
+ };
112
+
113
+ export const showConfirmationDialog = ({
114
+ title,
115
+ message,
116
+ okButtonText,
117
+ cancelButtonText,
118
+ confirmCompletion,
119
+ cancelCompletion,
120
+ }: CustomConfirmationDialog): void => {
121
+ if (isWeb()) {
122
+ if (cancelCompletion) {
123
+ handleCustomDialog({
124
+ messages: {
125
+ title,
126
+ message,
127
+ okButtonText,
128
+ cancelButtonText,
129
+ },
130
+ confirmCompletion,
131
+ cancelCompletion,
132
+ });
133
+ } else {
134
+ showWebDialog({ message, confirmCompletion, cancelCompletion });
135
+ }
136
+ } else {
137
+ showNativeDialog({
138
+ title,
139
+ message,
140
+ okButtonText,
141
+ cancelButtonText,
142
+ confirmCompletion,
143
+ cancelCompletion,
144
+ });
145
+ }
146
+ };
147
+
148
+ // #endregion "Confirmation Dialog"
@@ -1,148 +1 @@
1
- import { Alert } from "react-native";
2
- import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
3
- import { confirmationDialogStore } from "@applicaster/zapp-react-native-ui-components/Contexts/ConfirmDialogState";
4
-
5
- // #region "Alert Dialog"
6
-
7
- const globalAny: any = global;
8
-
9
- const showWebAlertDialog = ({ message, completion }: AlertWebDialog) => {
10
- globalAny?.alert?.(message);
11
- completion?.();
12
- };
13
-
14
- const showNativeAlertDialog = ({
15
- title,
16
- message,
17
- okButtonText,
18
- completion,
19
- }: AlertDialogType) => {
20
- Alert.alert(title, message, [
21
- {
22
- text: okButtonText,
23
- onPress: completion,
24
- },
25
- ]);
26
- };
27
-
28
- export const showAlertDialog = ({
29
- title,
30
- message,
31
- okButtonText,
32
- completion,
33
- }: AlertDialogType) => {
34
- if (isWeb()) {
35
- showWebAlertDialog({ message, completion });
36
- } else {
37
- showNativeAlertDialog({
38
- title,
39
- message,
40
- okButtonText,
41
- completion,
42
- });
43
- }
44
- };
45
-
46
- // #endregion "Alert Dialog"
47
-
48
- // #region "Confirmation Dialog"
49
-
50
- const showWebDialog = ({
51
- message,
52
- confirmCompletion,
53
- cancelCompletion,
54
- }: WebConfirmationDialog) => {
55
- const isConfirmed = globalAny?.confirm?.(message);
56
-
57
- if (isConfirmed) {
58
- confirmCompletion?.();
59
- } else {
60
- cancelCompletion?.();
61
- }
62
- };
63
-
64
- const showNativeDialog = ({
65
- title,
66
- message,
67
- okButtonText,
68
- cancelButtonText,
69
- confirmCompletion,
70
- cancelCompletion,
71
- }: ConfirmationDialog) => {
72
- Alert.alert(title, message, [
73
- {
74
- text: cancelButtonText,
75
- onPress: cancelCompletion,
76
- },
77
- {
78
- text: okButtonText,
79
- style: "destructive",
80
- onPress: confirmCompletion,
81
- },
82
- ]);
83
- };
84
-
85
- const handleCustomDialog = (data) => {
86
- const {
87
- showDialog,
88
- hideDialog,
89
- setConfirmAction,
90
- setCancelAction,
91
- setMessages,
92
- } = confirmationDialogStore.getState();
93
-
94
- setMessages(data.messages);
95
-
96
- if (data?.cancelCompletion) {
97
- setCancelAction(() => {
98
- data.cancelCompletion();
99
- hideDialog();
100
- });
101
- }
102
-
103
- if (data?.confirmCompletion) {
104
- setConfirmAction(() => {
105
- data.confirmCompletion();
106
- hideDialog();
107
- });
108
- }
109
-
110
- showDialog();
111
- };
112
-
113
- export const showConfirmationDialog = ({
114
- title,
115
- message,
116
- okButtonText,
117
- cancelButtonText,
118
- confirmCompletion,
119
- cancelCompletion,
120
- }: CustomConfirmationDialog): void => {
121
- if (isWeb()) {
122
- if (cancelCompletion) {
123
- handleCustomDialog({
124
- messages: {
125
- title,
126
- message,
127
- okButtonText,
128
- cancelButtonText,
129
- },
130
- confirmCompletion,
131
- cancelCompletion,
132
- });
133
- } else {
134
- showWebDialog({ message, confirmCompletion, cancelCompletion });
135
- }
136
- } else {
137
- showNativeDialog({
138
- title,
139
- message,
140
- okButtonText,
141
- cancelButtonText,
142
- confirmCompletion,
143
- cancelCompletion,
144
- });
145
- }
146
- };
147
-
148
- // #endregion "Confirmation Dialog"
1
+ export { showAlertDialog, showConfirmationDialog } from "./alert";
@@ -0,0 +1,62 @@
1
+ import { ContextKeysManager } from "./index";
2
+ import * as R from "ramda";
3
+
4
+ interface IResolver {
5
+ resolve: (string) => Promise<string | number | object>;
6
+ }
7
+
8
+ export class EntryResolver implements IResolver {
9
+ entry: ZappEntry;
10
+
11
+ constructor(entry: ZappEntry) {
12
+ this.entry = entry;
13
+ }
14
+
15
+ async resolve(key: string) {
16
+ if (!key || key.length === 0) {
17
+ return this.entry;
18
+ }
19
+
20
+ return R.view(R.lensPath(key.split(".")), this.entry);
21
+ }
22
+ }
23
+
24
+ export class ContextResolver implements IResolver {
25
+ resolve = async (compositeKey: string) =>
26
+ ContextKeysManager.instance.getKey(compositeKey);
27
+ }
28
+
29
+ export const resolveObjectValues = async (
30
+ data: Record<string, any>,
31
+ resolvers: Record<string, IResolver> = null
32
+ ) => {
33
+ const entries = Object.entries(data);
34
+
35
+ const exResolvers = { ctx: new ContextResolver(), ...resolvers };
36
+
37
+ const resolvedEntries = await Promise.all(
38
+ entries.map(async ([key, value]) => {
39
+ const regex = /@{([^/]+)\/([^}]*)}/;
40
+
41
+ if (typeof value !== "string") {
42
+ return [key, value];
43
+ }
44
+
45
+ const match = (value as string).match(regex);
46
+
47
+ if (match) {
48
+ const contextResolverName = match[1];
49
+ const compositeKey = match[2];
50
+
51
+ const resolver = exResolvers[contextResolverName] || exResolvers.ctx;
52
+ const resolvedValue = await resolver.resolve(compositeKey);
53
+
54
+ return [key, resolvedValue];
55
+ }
56
+
57
+ return [key, value];
58
+ })
59
+ );
60
+
61
+ return Object.fromEntries(resolvedEntries);
62
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "12.0.16",
3
+ "version": "12.0.17-rc.0",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "12.0.16",
31
+ "@applicaster/applicaster-types": "12.0.17-rc.0",
32
32
  "buffer": "^5.2.1",
33
33
  "camelize": "^1.0.0",
34
34
  "create-react-context": "^0.2.3",
@@ -5,14 +5,14 @@ import { ActionExecutorContext } from "@applicaster/zapp-react-native-utils/acti
5
5
 
6
6
  import { waitFor } from "@testing-library/react-native";
7
7
 
8
- const navigator = {
8
+ const mockNavigator = {
9
9
  getPathname: () => "pathnameMock",
10
10
  push: jest.fn(),
11
11
  data: {},
12
12
  };
13
13
 
14
14
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation", () => ({
15
- useNavigation: () => navigator,
15
+ useNavigation: () => mockNavigator,
16
16
  useRoute: () => ({ pathname: "pathnameMock" }),
17
17
  useProfilerLogging: () => {
18
18
  return jest.fn();
@@ -25,7 +25,16 @@ jest.mock("@applicaster/zapp-react-native-utils/analyticsUtils/", () => ({
25
25
  }),
26
26
  }));
27
27
 
28
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen", () => ({
29
+ useTargetScreenData: jest.fn(() => ({})),
30
+ useCurrentScreenData: jest.fn(() => ({})),
31
+ useScreenContext: jest.fn().mockReturnValue({ screen: {}, entry: {} }),
32
+ }));
33
+
28
34
  jest.mock("@applicaster/zapp-react-native-ui-components/Contexts", () => ({
35
+ ...jest.requireActual(
36
+ "@applicaster/zapp-react-native-ui-components/Contexts"
37
+ ),
29
38
  ZappPipesEntryContext: {
30
39
  useZappPipesContext: jest.fn(() => [undefined, jest.fn()]),
31
40
  },
@@ -16,6 +16,7 @@ import { ActionExecutorContext } from "@applicaster/zapp-react-native-utils/acti
16
16
  import { isFunction, noop } from "../../functionUtils";
17
17
  import { useSendAnalyticsOnPress } from "../analytics";
18
18
  import { logOnPress, warnEmptyContentType } from "./helpers";
19
+ import { useCurrentScreenData } from "../screen";
19
20
 
20
21
  /**
21
22
  * If onCellTap is defined execute the function and
@@ -44,6 +45,7 @@ export const useCellClick = ({
44
45
 
45
46
  const onCellTap: Option<Function> = React.useContext(CellTapContext);
46
47
  const actionExecutor = React.useContext(ActionExecutorContext);
48
+ const screenData = useCurrentScreenData();
47
49
 
48
50
  const cellSelectable = toBooleanWithDefaultTrue(
49
51
  component?.rules?.component_cells_selectable
@@ -78,7 +80,10 @@ export const useCellClick = ({
78
80
  logOnPress(selectedItem, pathname, component, onCellTap);
79
81
 
80
82
  if (selectedItem) {
81
- await actionExecutor?.handleEntryActions(selectedItem);
83
+ await actionExecutor?.handleEntryActions(selectedItem, {
84
+ component,
85
+ screenData,
86
+ });
82
87
  }
83
88
 
84
89
  if (selectedItem?.type?.value === "action") {
@@ -111,6 +116,7 @@ export const useCellClick = ({
111
116
  pathname,
112
117
  push,
113
118
  sendAnalyticsOnPress,
119
+ screenData,
114
120
  ]
115
121
  );
116
122