@adobe/aio-commerce-lib-admin-ui 0.2.0-beta-20260714082406 → 0.2.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.
@@ -48,6 +48,37 @@ function isControlFrame() {
48
48
  return isEmbeddedInHost() && !window.name;
49
49
  }
50
50
 
51
+ //#endregion
52
+ //#region source/web/react/result.ts
53
+ /** Creates a successful data result. */
54
+ function ok(data) {
55
+ return {
56
+ data,
57
+ error: null
58
+ };
59
+ }
60
+ /** Creates a failed data result. */
61
+ function error(message, options) {
62
+ return {
63
+ data: null,
64
+ error: new Error(message, options)
65
+ };
66
+ }
67
+ /** Creates a successful actions result. */
68
+ function okActions(actions) {
69
+ return {
70
+ actions,
71
+ error: null
72
+ };
73
+ }
74
+ /** Creates a failed actions result. */
75
+ function actionsError(message, options) {
76
+ return {
77
+ actions: null,
78
+ error: new Error(message, options)
79
+ };
80
+ }
81
+
51
82
  //#endregion
52
83
  //#region source/web/react/auth/context/ims-context.tsx
53
84
  const ImsContextValue = createContext(void 0);
@@ -55,14 +86,13 @@ const ImsContextValue = createContext(void 0);
55
86
  * Returns the IMS credentials provided by the host. Works inside the Commerce Admin and the
56
87
  * Experience Cloud shell.
57
88
  *
58
- * @throws If no host provides credentials (e.g. the app is running standalone, outside both the
59
- * Commerce Admin and the Experience Cloud shell).
89
+ * Returns an error when no host provides credentials.
60
90
  */
61
91
  function useIms() {
62
92
  const credentials = use(ImsContextValue);
63
- if (credentials === void 0) throw new Error("useIms must be used inside an ImsContextProvider.");
64
- if (!credentials) throw new Error("useIms requires running inside the Commerce Admin or the Experience Cloud shell, which provide the IMS credentials.");
65
- return credentials;
93
+ if (credentials === void 0) return error("useIms must be used inside an ImsContextProvider.");
94
+ if (!credentials) return error("useIms requires running inside the Commerce Admin or the Experience Cloud shell, which provide the IMS credentials.");
95
+ return ok(credentials);
66
96
  }
67
97
  /**
68
98
  * Provides the IMS credentials for a mounted Admin UI iframe app.
@@ -84,9 +114,7 @@ const SharedContextValue = createContext(void 0);
84
114
  * Returns the current Commerce shared context provider state.
85
115
  */
86
116
  function useInternalSharedContext() {
87
- const context = use(SharedContextValue);
88
- if (context === void 0) throw new Error("useSharedContext must be used inside a SharedContextProvider, which is only available in the Commerce Admin.");
89
- return context;
117
+ return use(SharedContextValue);
90
118
  }
91
119
  /**
92
120
  * Returns the current Commerce shared context. The guest connection is already established by
@@ -96,26 +124,29 @@ function useInternalSharedContext() {
96
124
  * Prefer a purpose-built hook ({@link useCommerce}, {@link useMassActionContext},
97
125
  * {@link useOrderViewButtonContext}) when one covers what you need.
98
126
  *
99
- * @throws If used outside a {@link SharedContextProvider}.
100
- *
101
127
  * @example
102
128
  * ```tsx
103
129
  * import { useSharedContext } from "@adobe/aio-commerce-lib-admin-ui/web";
104
130
  *
105
131
  * function ImsTokenLabel() {
106
- * const { sharedContext } = useSharedContext();
107
- * return <span>{sharedContext.get("imsToken")}</span>;
132
+ * const { data, error } = useSharedContext();
133
+ * if (error) return null;
134
+ * return <span>{data.sharedContext.get("imsToken")}</span>;
108
135
  * }
109
136
  * ```
110
137
  */
111
138
  function useSharedContext() {
112
- const { extensionId, guestConnection } = useInternalSharedContext();
113
- const sharedContext = useLiveSharedContext(guestConnection);
114
- return {
115
- extensionId,
116
- host: guestConnection.host,
117
- sharedContext
118
- };
139
+ const context = useInternalSharedContext();
140
+ const sharedContext = useLiveSharedContext(context?.guestConnection);
141
+ return useMemo(() => {
142
+ if (!context) return error("useSharedContext must be used inside a SharedContextProvider, which is only available in the Commerce Admin.");
143
+ if (!sharedContext) return error("The Commerce host did not provide a shared context for this extension.");
144
+ return ok({
145
+ extensionId: context.extensionId,
146
+ host: context.guestConnection.host,
147
+ sharedContext
148
+ });
149
+ }, [context, sharedContext]);
119
150
  }
120
151
  /**
121
152
  * Tracks the live UIX `sharedContext` for a connection.
@@ -127,7 +158,10 @@ function useSharedContext() {
127
158
  * @param guestConnection - The established guest connection.
128
159
  */
129
160
  function useLiveSharedContext(guestConnection) {
130
- return useSyncExternalStore(useCallback((onContextChange) => guestConnection.addEventListener("contextchange", onContextChange), [guestConnection]), () => guestConnection.sharedContext);
161
+ return useSyncExternalStore(useCallback((onContextChange) => {
162
+ if (!guestConnection) return () => void 0;
163
+ return guestConnection.addEventListener("contextchange", onContextChange);
164
+ }, [guestConnection]), () => guestConnection?.sharedContext ?? null);
131
165
  }
132
166
  /**
133
167
  * Provides the Commerce shared context for a mounted Admin UI iframe app.
@@ -149,28 +183,33 @@ function SharedContextProvider(props) {
149
183
  //#endregion
150
184
  //#region source/web/react/promise-cache.ts
151
185
  /**
152
- * Creates a keyed cache that memoizes in-flight, resolved, and rejected promises, giving `use()`
186
+ * Creates a keyed cache that memoizes in-flight, successful, and failed promises, giving `use()`
153
187
  * the reference-stable promise it needs while suspended and single-flighting side-effecting
154
188
  * establishment calls across re-renders, remounts, and StrictMode double-invocation.
155
189
  *
156
- * Rejections are retained (not evicted) so `use()` replays them to an error boundary instead of
157
- * suspending forever on a fresh pending promise. Retry a failed key via {@link RetryablePromiseCache.evict}.
190
+ * Promise rejections are failures by default. Pass `isFailure` when a fulfilled value, such as an
191
+ * error result, must also be retryable. Failed entries remain cached until `evictIfFailed` is
192
+ * called, so retries never replace a stable promise during render.
193
+ *
194
+ * @param isFailure - Determines whether a fulfilled value represents a failure.
158
195
  */
159
- function createRetryablePromiseCache() {
196
+ function createRetryablePromiseCache(isFailure = () => false) {
160
197
  const cache = /* @__PURE__ */ new Map();
161
198
  return {
162
- evictIfRejected(key) {
163
- if (cache.get(key)?.rejected) cache.delete(key);
199
+ evictIfFailed(key) {
200
+ if (cache.get(key)?.failed) cache.delete(key);
164
201
  },
165
202
  get(key, create) {
166
203
  const cached = cache.get(key);
167
204
  if (cached) return cached.promise;
168
205
  const entry = {
169
- promise: create(),
170
- rejected: false
206
+ failed: false,
207
+ promise: create()
171
208
  };
172
- entry.promise.catch(() => {
173
- entry.rejected = true;
209
+ entry.promise.then((value) => {
210
+ entry.failed = isFailure(value);
211
+ }).catch(() => {
212
+ entry.failed = true;
174
213
  });
175
214
  cache.set(key, entry);
176
215
  return entry.promise;
@@ -180,7 +219,7 @@ function createRetryablePromiseCache() {
180
219
 
181
220
  //#endregion
182
221
  //#region source/web/react/commerce/hooks/use-commerce.ts
183
- const commerceHosts = createRetryablePromiseCache();
222
+ const commerceHosts = createRetryablePromiseCache((result) => result.error !== null);
184
223
  /**
185
224
  * Returns the cached Commerce Admin host promise for an extension, resolving it once over the
186
225
  * given guest connection. The value is static for the lifetime of the connection.
@@ -189,24 +228,31 @@ const commerceHosts = createRetryablePromiseCache();
189
228
  * @param connection - The established guest connection.
190
229
  */
191
230
  function getCommerceHostPromise(extensionId, connection) {
192
- const { integration } = connection.host;
193
- if (!integration) throw new Error("The host does not provide the integration API needed to resolve the Commerce host.");
194
- return commerceHosts.get(extensionId, () => integration.getCommerceHost());
231
+ return commerceHosts.get(extensionId, async () => {
232
+ const { integration } = connection.host;
233
+ if (!integration) return error("The host does not provide the integration API needed to resolve the Commerce host.");
234
+ try {
235
+ return ok({ commerceHost: await integration.getCommerceHost() });
236
+ } catch (cause) {
237
+ return error("Failed to resolve the Commerce host.", { cause });
238
+ }
239
+ });
195
240
  }
196
241
  /** Drops a failed Commerce host resolution for `extensionId`, so a later render retries it. */
197
242
  function retryCommerceHost(extensionId) {
198
- commerceHosts.evictIfRejected(extensionId);
243
+ commerceHosts.evictIfFailed(extensionId);
199
244
  }
200
245
  /**
201
246
  * Returns the host (domain) of the Commerce Admin the extension is embedded in, resolving it over
202
247
  * the guest connection.
203
248
  *
204
- * @throws If used outside a Commerce Admin UI frame, or when the host does not expose the
205
- * Commerce integration API.
249
+ * Returns an error when used outside a Commerce Admin UI frame, when the host does not expose the
250
+ * Commerce integration API, or when resolving the host fails.
206
251
  */
207
252
  function useCommerce() {
208
- const { extensionId, guestConnection } = useInternalSharedContext();
209
- return { commerceHost: use(getCommerceHostPromise(extensionId, guestConnection)) };
253
+ const context = useInternalSharedContext();
254
+ if (!context) return error("useCommerce requires running inside the Commerce Admin.");
255
+ return use(getCommerceHostPromise(context.extensionId, context.guestConnection));
210
256
  }
211
257
 
212
258
  //#endregion
@@ -215,28 +261,31 @@ function useCommerce() {
215
261
  * Returns the context for a mass-action extension point: the selected row IDs the action was
216
262
  * triggered with. The value is read from the host-provided Commerce context.
217
263
  *
218
- * @throws If used outside the Commerce shared context, or when that context does not include a
219
- * mass-action selection.
264
+ * Returns an error outside the Commerce shared context, or when the mass-action selection is
265
+ * missing, empty, or contains a non-string row ID.
220
266
  */
221
267
  function useMassActionContext() {
222
- const { sharedContext } = useSharedContext();
268
+ const { data, error: contextError } = useSharedContext();
223
269
  return useMemo(() => {
224
- const selectedIds = sharedContext.get("selectedIds");
225
- if (!Array.isArray(selectedIds)) throw new Error("Could not find `selectedIds` in the Commerce shared context. Is this frame running as a mass-action extension point?");
226
- return { selectedIds };
227
- }, [sharedContext]);
270
+ if (contextError) return error(contextError.message, { cause: contextError });
271
+ const selectedIds = data.sharedContext.get("selectedIds");
272
+ if (!Array.isArray(selectedIds)) return error("Could not find `selectedIds` in the Commerce shared context. Is this frame running as a mass-action extension point?");
273
+ if (selectedIds.length === 0) return error("No rows selected. A mass-action extension point must be triggered with at least one selected row.");
274
+ if (selectedIds.some((id) => typeof id !== "string")) return error("Some of the `selectedIds` in the Commerce shared context are not strings. All selected row IDs must be strings.");
275
+ return ok({ selectedIds });
276
+ }, [data, contextError]);
228
277
  }
229
278
  /**
230
279
  * Returns the context for an order view-button extension point: the order ID the button was
231
280
  * triggered from.
232
281
  *
233
- * @throws If no order ID is present in the page URL.
282
+ * Returns an error when no order ID is present in the page URL.
234
283
  */
235
284
  function useOrderViewButtonContext() {
236
285
  return useMemo(() => {
237
286
  const orderId = parseOrderId(globalThis.location.href);
238
- if (orderId === null) throw new Error("Could not find an order ID. Is this frame running as an order view-button extension point?");
239
- return { orderId };
287
+ if (orderId === null) return error("Could not find an order ID. Is this frame running as an order view-button extension point?");
288
+ return ok({ orderId });
240
289
  }, []);
241
290
  }
242
291
 
@@ -245,51 +294,39 @@ function useOrderViewButtonContext() {
245
294
  /**
246
295
  * Returns typed helpers for interacting with the Commerce Admin host.
247
296
  *
248
- * @throws If called before the guest connection is established, or when the host frame actions
249
- * are unavailable.
250
- *
251
297
  * @example
252
298
  * ```tsx
253
299
  * import { useHostConnection } from "@adobe/aio-commerce-lib-admin-ui/web";
254
300
  *
255
301
  * function DoneButton() {
256
- * const { close } = useHostConnection();
257
- * return <button onClick={() => void close()}>Done</button>;
302
+ * const { actions, error } = useHostConnection();
303
+ * if (error) return null;
304
+ * return <button onClick={actions.close}>Done</button>;
258
305
  * }
259
306
  * ```
260
307
  */
261
308
  function useHostConnection() {
262
- const { host } = useSharedContext();
309
+ const { data, error: contextError } = useSharedContext();
263
310
  return useMemo(() => {
264
- const { field } = host;
265
- const requireField = () => {
266
- if (!field) throw new Error("Host frame actions are unavailable. They require an established guest connection with a host that exposes frame actions.");
267
- return field;
268
- };
269
- return {
270
- close: () => requireField().close(),
271
- closeWithError: () => requireField().onError()
272
- };
273
- }, [host]);
311
+ if (contextError) return actionsError(contextError.message, { cause: contextError });
312
+ const { field } = data.host;
313
+ if (!field) return actionsError("Host frame actions are unavailable. They require an established guest connection with a host that exposes frame actions.");
314
+ return okActions({
315
+ close: () => field.close(),
316
+ closeWithError: () => field.onError()
317
+ });
318
+ }, [data, contextError]);
274
319
  }
275
320
 
276
321
  //#endregion
277
322
  //#region source/web/react/routing/lib.ts
278
323
  const HASH_ROUTE_PREFIX_PATTERN = /^[#/]+/u;
279
324
  /**
280
- * Determines if a given route is an index route.
281
- * @param route The route to check.
282
- */
283
- function isIndexRoute(route) {
284
- return "index" in route && route.index;
285
- }
286
- /**
287
325
  * Returns the path for a given route, removing any leading hash or slash characters.
288
326
  * @param route The route to get the path for.
289
327
  */
290
328
  function getRoutePath(route) {
291
- if (isIndexRoute(route)) return "/";
292
- return route.path.replace(HASH_ROUTE_PREFIX_PATTERN, "");
329
+ return route.path.replace(HASH_ROUTE_PREFIX_PATTERN, "") || "/";
293
330
  }
294
331
  /**
295
332
  * Returns the "to" path for a given route, removing any leading hash or slash characters.
@@ -431,7 +468,7 @@ function useGuestConnection(extensionId) {
431
468
  }
432
469
  /** Drops a failed connection for `extensionId`, so a later render re-attaches. */
433
470
  function retryGuestConnection(extensionId) {
434
- guestConnections.evictIfRejected(extensionId);
471
+ guestConnections.evictIfFailed(extensionId);
435
472
  }
436
473
 
437
474
  //#endregion
@@ -622,9 +659,10 @@ function CommerceGuestConnection(props) {
622
659
  }
623
660
  /** Renders IMS-gated route content for a Commerce Admin UI frame. */
624
661
  function CommerceExtensionContent() {
625
- const { sharedContext } = useSharedContext();
662
+ const { data, error } = useSharedContext();
663
+ if (error) throw error;
626
664
  return /* @__PURE__ */ jsx(ImsContextProvider, {
627
- credentials: resolveCommerceImsCredentials(sharedContext),
665
+ credentials: resolveCommerceImsCredentials(data.sharedContext),
628
666
  children: /* @__PURE__ */ jsx(Outlet, {})
629
667
  });
630
668
  }
@@ -780,11 +818,12 @@ function Entrypoint(props) {
780
818
  *
781
819
  * createExtensionApp({
782
820
  * metadata: { extensionId: "my-extension-id" },
783
- * routes: [{ index: true, element: <MainPage /> }],
821
+ * menu: <MainPage />,
784
822
  * });
785
823
  * ```
786
824
  */
787
- function createExtensionApp({ metadata, routes, root: customRoot }) {
825
+ function createExtensionApp({ menu, metadata, routes = [], root: customRoot }) {
826
+ if (menu !== void 0 && routes.some((route) => getRouteTo(route.path) === "/")) throw new Error("The \"/\" route is reserved for the menu. Pass its element through the menu option instead.");
788
827
  const rootElement = customRoot ?? document.getElementById("root");
789
828
  if (!rootElement) throw new Error("Could not find an element with id \"root\".");
790
829
  const root = createRoot(rootElement);
@@ -793,7 +832,10 @@ function createExtensionApp({ metadata, routes, root: customRoot }) {
793
832
  extensionId: metadata.extensionId,
794
833
  initialConfigurationPromise,
795
834
  runtime
796
- }), routes);
835
+ }), menu === void 0 ? routes : [{
836
+ element: menu,
837
+ path: "/"
838
+ }, ...routes]);
797
839
  root.render(/* @__PURE__ */ jsx(StrictMode, { children: /* @__PURE__ */ jsx(RouterProvider, { router }) }));
798
840
  };
799
841
  try {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@adobe/aio-commerce-lib-admin-ui",
3
3
  "type": "module",
4
4
  "author": "Adobe Inc.",
5
- "version": "0.2.0-beta-20260714082406",
5
+ "version": "0.2.0",
6
6
  "private": false,
7
7
  "engines": {
8
8
  "node": ">=22 <=24"
@@ -113,8 +113,8 @@
113
113
  "ky": "^1.9.0",
114
114
  "react-error-boundary": "^6.1.2",
115
115
  "valibot": "^1.1.0",
116
- "@adobe/aio-commerce-lib-api": "1.3.0-beta-20260714082406",
117
- "@adobe/aio-commerce-lib-core": "1.2.0-beta-20260714082406"
116
+ "@adobe/aio-commerce-lib-api": "1.3.0",
117
+ "@adobe/aio-commerce-lib-core": "1.2.0"
118
118
  },
119
119
  "peerDependencies": {
120
120
  "@react-spectrum/s2": "^1.5.1",
@@ -139,12 +139,12 @@
139
139
  "react": "^19.2.7",
140
140
  "react-dom": "^19.2.7",
141
141
  "typescript": "^6.0.0",
142
- "@aio-commerce-sdk/common-utils": "0.2.6-beta-20260714082406",
142
+ "@aio-commerce-sdk/common-utils": "0.2.6",
143
143
  "@aio-commerce-sdk/config-tsdown": "1.0.1",
144
144
  "@aio-commerce-sdk/config-typedoc": "1.0.0",
145
145
  "@aio-commerce-sdk/config-typescript": "1.0.0",
146
146
  "@aio-commerce-sdk/config-vitest": "1.0.0",
147
- "@aio-commerce-sdk/scripting-utils": "0.3.4-beta-20260714082406",
147
+ "@aio-commerce-sdk/scripting-utils": "0.3.4",
148
148
  "@aio-commerce-sdk/scripts": "0.1.0"
149
149
  },
150
150
  "sideEffects": false,