@applicaster/zapp-react-native-utils 16.0.0-rc.72 → 16.0.0-rc.74

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.
@@ -135,7 +135,8 @@ function generateFieldsFromDefaults(label, defaults, fields) {
135
135
  function generateFieldsFromDefaultsWithoutPrefixedLabel(
136
136
  label,
137
137
  defaults,
138
- fields
138
+ fields,
139
+ { withStyle = false } = {}
139
140
  ) {
140
141
  const keyPrefix = toSnakeCase(label);
141
142
 
@@ -151,7 +152,9 @@ function generateFieldsFromDefaultsWithoutPrefixedLabel(
151
152
  const generatedField = {
152
153
  ...fieldProps,
153
154
  label: capitalizeFirstLetter(field.suffix),
154
- key: `${keyPrefix}_${toSnakeCase(field.suffix)}`,
155
+ key: withStyle
156
+ ? `${keyPrefix}_style_${toSnakeCase(field.suffix)}`
157
+ : `${keyPrefix}_${toSnakeCase(field.suffix)}`,
155
158
  initial_value: R.when(R.is(Array), R.head)(initialValue),
156
159
  };
157
160
 
@@ -164,7 +167,9 @@ function generateFieldsFromDefaultsWithoutPrefixedLabel(
164
167
  const section = condition.section ? `${condition.section}/` : "";
165
168
 
166
169
  return {
167
- key: `${section}${keyPrefix}_${toSnakeCase(condition.key)}`,
170
+ key: withStyle
171
+ ? `${section}${keyPrefix}_style_${toSnakeCase(condition.key)}`
172
+ : `${section}${keyPrefix}_${toSnakeCase(condition.key)}`,
168
173
  condition_value: condition.value,
169
174
  };
170
175
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "16.0.0-rc.72",
3
+ "version": "16.0.0-rc.74",
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.72",
30
+ "@applicaster/applicaster-types": "16.0.0-rc.74",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -87,7 +87,31 @@ export function useEntryActionState(
87
87
  useEffect(() => {
88
88
  if (!action) return undefined;
89
89
 
90
- const subscription = observeEntryState(action, entry).subscribe(setState);
90
+ // An action's stream is third-party code. Subscribing with only a `next`
91
+ // handler lets RxJS rethrow an error notification asynchronously, where no
92
+ // error boundary catches it - one plugin faulting would take the app down
93
+ // rather than this one button. The button keeps its last known state.
94
+ let subscription;
95
+
96
+ try {
97
+ subscription = observeEntryState(action, entry).subscribe({
98
+ next: setState,
99
+ error: (error) =>
100
+ logger.warning({
101
+ message:
102
+ "useEntryActionState: the action's state stream failed - the button keeps its last state",
103
+ data: { entryId, error },
104
+ }),
105
+ });
106
+ } catch (error) {
107
+ logger.warning({
108
+ message:
109
+ "useEntryActionState: could not observe the action - the button keeps its initial state",
110
+ data: { entryId, error },
111
+ });
112
+
113
+ return undefined;
114
+ }
91
115
 
92
116
  return () => subscription.unsubscribe();
93
117
  // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
@@ -1,4 +1,6 @@
1
1
  export {
2
+ canRenderActionIcon,
3
+ resolveActionAsset,
2
4
  isActionAvailableFor,
3
5
  observeEntryState,
4
6
  uiActionsRegistry,
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable no-dupe-class-members */
2
+ import type { ComponentType } from "react";
2
3
  import { Observable } from "rxjs";
3
4
  import { createLogger } from "../logger";
4
5
 
@@ -119,12 +120,101 @@ export function isActionAvailableFor(
119
120
  action: RegisteredActionValue | undefined,
120
121
  entry: ZappEntry | ZappFeed
121
122
  ): boolean {
122
- const predicate =
123
- action?.isSupportDownloads ??
124
- action?.isActionSupported ??
125
- action?.isActionAvailable;
123
+ // The first name the action defines *as a function* answers. Nullish
124
+ // coalescing would let a plugin shipping one of these as a plain flag
125
+ // short-circuit the chain and mask a real predicate behind it.
126
+ const predicate = [
127
+ action?.isSupportDownloads,
128
+ action?.isActionSupported,
129
+ action?.isActionAvailable,
130
+ ].find((candidate) => typeof candidate === "function");
126
131
 
127
- return typeof predicate === "function" ? Boolean(predicate(entry)) : true;
132
+ if (!predicate) {
133
+ return true;
134
+ }
135
+
136
+ try {
137
+ return Boolean(predicate(entry));
138
+ } catch (error) {
139
+ // Third-party code, called during render. Showing the action is the
140
+ // recoverable answer: a button that does nothing beats a screen that does
141
+ // not render.
142
+ log_warning(
143
+ "isActionAvailableFor: availability check threw - the action is shown",
144
+ { entryId: (entry as ZappEntry)?.id, error }
145
+ );
146
+
147
+ return true;
148
+ }
149
+ }
150
+
151
+ /** How an action's asset should be drawn, or `null` if it cannot be. */
152
+ export type ResolvedActionAsset =
153
+ | { uri: string }
154
+ | { Component: ComponentType<any> }
155
+ | null;
156
+
157
+ /**
158
+ * Interprets `CellActionEntryState["asset"]`.
159
+ *
160
+ * It is either something an image can take - a URI, or a per-flavour array -
161
+ * or a component that draws itself. A plain object is neither: a locale or
162
+ * state map arrives that way, and handing one to JSX throws "Element type is
163
+ * invalid".
164
+ *
165
+ * A React *element* carries the same `$$typeof` marker as a `memo`,
166
+ * `forwardRef` or `lazy` result, so `asset: <Icon/>` where `asset: Icon` was
167
+ * meant would pass a naive check and throw exactly that. Elements are told
168
+ * apart by carrying `props`.
169
+ *
170
+ * One function so that "can this draw?" and "draw it" cannot disagree.
171
+ */
172
+ export function resolveActionAsset(asset: unknown): ResolvedActionAsset {
173
+ if (typeof asset === "string") {
174
+ return asset.length > 0 ? { uri: asset } : null;
175
+ }
176
+
177
+ if (Array.isArray(asset)) {
178
+ const first = asset.find(
179
+ (candidate) => typeof candidate === "string" && candidate.length > 0
180
+ );
181
+
182
+ return first ? { uri: first as string } : null;
183
+ }
184
+
185
+ if (typeof asset === "function") {
186
+ return { Component: asset as ComponentType<any> };
187
+ }
188
+
189
+ if (
190
+ typeof asset === "object" &&
191
+ asset !== null &&
192
+ "$$typeof" in asset &&
193
+ !("props" in asset)
194
+ ) {
195
+ return { Component: asset as ComponentType<any> };
196
+ }
197
+
198
+ return null;
199
+ }
200
+
201
+ /**
202
+ * Whether the action has something an icon-only button can actually draw.
203
+ *
204
+ * An overlay button is an icon and nothing else, so an action whose state
205
+ * carries no usable asset renders as nothing at all - legal for an entry action
206
+ * declared with a title and no `iconURL`. Surfaces that must keep an action
207
+ * reachable ask this before putting it somewhere it can only appear as an icon.
208
+ */
209
+ export function canRenderActionIcon(
210
+ action: RegisteredActionValue | undefined,
211
+ entry: ZappEntry | ZappFeed
212
+ ): boolean {
213
+ try {
214
+ return resolveActionAsset(action?.initialEntryState?.(entry)?.asset) !== null; // prettier-ignore
215
+ } catch {
216
+ return false;
217
+ }
128
218
  }
129
219
 
130
220
  class UIActionsRegistry {