@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.
package/CHANGELOG.md CHANGED
@@ -1,50 +1,32 @@
1
1
  # @adobe/aio-commerce-lib-admin-ui
2
2
 
3
- ## 0.2.0-beta-20260714082406
4
- ### Minor Changes
5
-
3
+ ## 0.2.0
6
4
 
5
+ ### Minor Changes
7
6
 
8
7
  - [#528](https://github.com/adobe/aio-commerce-sdk/pull/528) [`0bb7d15`](https://github.com/adobe/aio-commerce-sdk/commit/0bb7d150961f631116b1ac421124960b5b857896) Thanks [@oshmyheliuk](https://github.com/oshmyheliuk)! - Add `getGridColumnAclResourceId`, `getMassActionAclResourceId`, and `getOrderViewButtonAclResourceId` helpers to derive the Commerce ACL resource id for protected grid columns, mass actions, and order view buttons, mirroring `getMenuAclResourceId`.
9
8
 
10
-
11
-
12
9
  - [#522](https://github.com/adobe/aio-commerce-sdk/pull/522) [`4396a64`](https://github.com/adobe/aio-commerce-sdk/commit/4396a647ad22dba063cdf03a16e04aef9387212f) Thanks [@oshmyheliuk](https://github.com/oshmyheliuk)! - Add `getAdminUiPermissionClient` for checking ACL permissions at runtime (SPA bootstrap and runtime action entry). Includes in-process TTL cache, in-flight request deduplication, and deny-by-default on errors. Also exports `getAclResourceId` for deterministic ACL resource id derivation from `metadata.id`.
13
10
 
14
-
15
-
16
11
  - [#542](https://github.com/adobe/aio-commerce-sdk/pull/542) [`c15b50f`](https://github.com/adobe/aio-commerce-sdk/commit/c15b50f667e6d1288270dbd4192c150db0ff0914) Thanks [@iivvaannxx](https://github.com/iivvaannxx)! - Add browser helpers for Admin UI iframe extensions.
17
12
 
18
-
19
-
20
13
  - [#508](https://github.com/adobe/aio-commerce-sdk/pull/508) [`c36f825`](https://github.com/adobe/aio-commerce-sdk/commit/c36f825aff7329fc2831d8aaca032d17bbee373a) Thanks [@iivvaannxx](https://github.com/iivvaannxx)! - Add `@adobe/aio-commerce-lib-admin-ui/menu` entrypoint with named constants for Commerce Admin menu IDs (`MENU_SALES`, `MENU_CATALOG`, etc.), a `COMMERCE_MENUS` collection, a `CommerceMenu` type, and an `isCommerceMenu` type guard.
21
14
 
22
-
23
-
24
15
  - [#516](https://github.com/adobe/aio-commerce-sdk/pull/516) [`bdcced9`](https://github.com/adobe/aio-commerce-sdk/commit/bdcced9297ae5e1b87ce86490a2b6fad76b2e341) Thanks [@obarcelonap](https://github.com/obarcelonap)! - Add `order-view-buttons` entrypoint with request parsing and response builders for `commerce/backend-ui/2` order view button handlers.
25
16
 
26
-
27
-
28
17
  - [#551](https://github.com/adobe/aio-commerce-sdk/pull/551) [`a66f389`](https://github.com/adobe/aio-commerce-sdk/commit/a66f3892622584d27cd6b5d2fc633e15bf9942a8) Thanks [@asalloum5](https://github.com/asalloum5)! - Add `enableAdminUiSdk` to the Admin UI API client, which enables the Admin UI SDK in Commerce via `PUT /V1/adminuisdk/config`.
29
18
 
30
-
31
-
32
19
  - [#486](https://github.com/adobe/aio-commerce-sdk/pull/486) [`413da36`](https://github.com/adobe/aio-commerce-sdk/commit/413da36dcd83e5a41c194f5af50611097659e725) Thanks [@obarcelonap](https://github.com/obarcelonap)! - Add request and response builders for the `commerce/backend-ui/2` grid column wire contract.
33
20
 
34
-
35
-
36
21
  - [#546](https://github.com/adobe/aio-commerce-sdk/pull/546) [`ca2c4fe`](https://github.com/adobe/aio-commerce-sdk/commit/ca2c4febcdfbe52b933775f08e9a53765fc306ab) Thanks [@obarcelonap](https://github.com/obarcelonap)! - Remove `extensionUrl` from extension registration params. The Commerce backend now derives the view URL from the App Registry, so the field has no effect for v2 installed apps.
37
22
 
38
-
39
-
40
- - [#492](https://github.com/adobe/aio-commerce-sdk/pull/492) [`ce63b2b`](https://github.com/adobe/aio-commerce-sdk/commit/ce63b2b46f7ef1f2f68780b13e52d1c13fd5fe6e) Thanks [@jcuerdo](https://github.com/jcuerdo)! - Add `mass-actions` entrypoint with wire contract helpers for `commerce/backend-ui/2` mass actions. Parse worker action requests with `parseMassActionRequest`, build success/error responses with `okMassActionResponse`/`errorMassActionResponse`, and parse iframe selection parameters with `parseMassActionSelection`.
41
-
23
+ - [#492](https://github.com/adobe/aio-commerce-sdk/pull/492) [`ce63b2b`](https://github.com/adobe/aio-commerce-sdk/commit/ce63b2b46f7ef1f2f68780b13e52d1c13fd5fe6e) Thanks [@jcuerdo](https://github.com/jcuerdo)! - Add `mass-actions` entrypoint with wire contract helpers for `commerce/backend-ui/2` mass actions. Parse worker action requests with `parseMassActionRequest`, and build success/error responses with `okMassActionResponse`/`errorMassActionResponse`.
42
24
 
43
25
  ### Patch Changes
44
26
 
45
27
  - Updated dependencies [[`19400fb`](https://github.com/adobe/aio-commerce-sdk/commit/19400fbdfc03c5bbfc30f995a2acdd4272c5e032), [`ce7768e`](https://github.com/adobe/aio-commerce-sdk/commit/ce7768e0ddc6ca35ad6ccec60f584c8bb5a743f0), [`4396a64`](https://github.com/adobe/aio-commerce-sdk/commit/4396a647ad22dba063cdf03a16e04aef9387212f), [`e85bec4`](https://github.com/adobe/aio-commerce-sdk/commit/e85bec40f1eaa91dd6bb7bc6954b2ecfc3ea4029), [`413da36`](https://github.com/adobe/aio-commerce-sdk/commit/413da36dcd83e5a41c194f5af50611097659e725)]:
46
- - @adobe/aio-commerce-lib-core@1.2.0-beta-20260714082406
47
- - @adobe/aio-commerce-lib-api@1.3.0-beta-20260714082406
28
+ - @adobe/aio-commerce-lib-core@1.2.0
29
+ - @adobe/aio-commerce-lib-api@1.3.0
48
30
 
49
31
  ## 0.1.0
50
32
 
@@ -51,58 +51,6 @@ function getMassActionAclResourceId(metadataId, entity, actionId) {
51
51
  return `${appRoot}_${entity}_massactions_${require_acl_resource_id.sanitizeSegment(actionId)}`;
52
52
  }
53
53
 
54
- //#endregion
55
- //#region source/mass-actions/view/schema.ts
56
- /**
57
- * Schema for the `selection` query parameter Commerce appends to the iframe URL
58
- * of a view mass action.
59
- *
60
- * Commerce serializes the selection as a JSON-encoded string:
61
- * `?selection={"ids":["000000001"],"gridType":"customer"}`
62
- */
63
- const MassActionSelectionSchema = valibot.object({
64
- gridType: valibot.picklist([
65
- "order",
66
- "product",
67
- "customer"
68
- ]),
69
- ids: valibot.pipe(valibot.array(require_schemas.nonEmptyStringValueSchema("id")), valibot.minLength(1, "The value of \"ids\" must contain at least one entry"))
70
- });
71
-
72
- //#endregion
73
- //#region source/mass-actions/view/presets.ts
74
- /**
75
- * Parses the `selection` query parameter Commerce appends to the iframe URL of
76
- * a view mass action.
77
- *
78
- * Commerce serializes the selection as a JSON string:
79
- * `?selection={"ids":["000000001","000000002"],"gridType":"customer"}`
80
- *
81
- * Throws a `CommerceSdkValidationError` if the input is missing, not valid
82
- * JSON, or does not match the expected shape.
83
- *
84
- * @param rawSelection - The raw string value of the `selection` query parameter.
85
- *
86
- * @example
87
- * ```ts
88
- * import { parseMassActionSelection } from "@adobe/aio-commerce-lib-admin-ui/mass-actions";
89
- *
90
- * // In your SPA route handler:
91
- * const raw = new URLSearchParams(window.location.search).get("selection");
92
- * const { ids, gridType } = parseMassActionSelection(raw);
93
- * ```
94
- */
95
- function parseMassActionSelection(rawSelection) {
96
- if (rawSelection === null || rawSelection === void 0) throw new Error("Invalid mass action selection: the selection query parameter is missing.");
97
- let parsed;
98
- try {
99
- parsed = JSON.parse(String(rawSelection));
100
- } catch (error) {
101
- throw new Error(`Invalid mass action selection: expected a JSON string, got: ${String(rawSelection)}`, { cause: error });
102
- }
103
- return require_utils.parseOrThrow(MassActionSelectionSchema, parsed, "Invalid mass action selection");
104
- }
105
-
106
54
  //#endregion
107
55
  //#region source/mass-actions/worker/schema.ts
108
56
  /**
@@ -180,9 +128,7 @@ function massActionErrorResponse(statusCode, errorMessage) {
180
128
  //#endregion
181
129
  exports.MassActionGridTypeSchema = MassActionGridTypeSchema;
182
130
  exports.MassActionRequestSchema = MassActionRequestSchema;
183
- exports.MassActionSelectionSchema = MassActionSelectionSchema;
184
131
  exports.getMassActionAclResourceId = getMassActionAclResourceId;
185
132
  exports.massActionErrorResponse = massActionErrorResponse;
186
133
  exports.okMassActionResponse = okMassActionResponse;
187
- exports.parseMassActionRequest = parseMassActionRequest;
188
- exports.parseMassActionSelection = parseMassActionSelection;
134
+ exports.parseMassActionRequest = parseMassActionRequest;
@@ -42,47 +42,6 @@ import { ErrorResponse, SuccessResponse } from "@adobe/aio-commerce-lib-core/res
42
42
  */
43
43
  declare function getMassActionAclResourceId(metadataId: string, entity: AdminUiEntity, actionId: string): string;
44
44
  //#endregion
45
- //#region source/mass-actions/view/schema.d.ts
46
- /**
47
- * Schema for the `selection` query parameter Commerce appends to the iframe URL
48
- * of a view mass action.
49
- *
50
- * Commerce serializes the selection as a JSON-encoded string:
51
- * `?selection={"ids":["000000001"],"gridType":"customer"}`
52
- */
53
- declare const MassActionSelectionSchema: v.ObjectSchema<{
54
- readonly gridType: v.PicklistSchema<["order", "product", "customer"], undefined>;
55
- readonly ids: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<`Expected a string value for '${string}'`>, v.NonEmptyAction<string, `The value of "${string}" must not be empty`>]>, undefined>, v.MinLengthAction<string[], 1, "The value of \"ids\" must contain at least one entry">]>;
56
- }, undefined>;
57
- //#endregion
58
- //#region source/mass-actions/view/types.d.ts
59
- /** Parsed `selection` query parameter appended by Commerce to the iframe URL. */
60
- type MassActionSelection = v.InferOutput<typeof MassActionSelectionSchema>;
61
- //#endregion
62
- //#region source/mass-actions/view/presets.d.ts
63
- /**
64
- * Parses the `selection` query parameter Commerce appends to the iframe URL of
65
- * a view mass action.
66
- *
67
- * Commerce serializes the selection as a JSON string:
68
- * `?selection={"ids":["000000001","000000002"],"gridType":"customer"}`
69
- *
70
- * Throws a `CommerceSdkValidationError` if the input is missing, not valid
71
- * JSON, or does not match the expected shape.
72
- *
73
- * @param rawSelection - The raw string value of the `selection` query parameter.
74
- *
75
- * @example
76
- * ```ts
77
- * import { parseMassActionSelection } from "@adobe/aio-commerce-lib-admin-ui/mass-actions";
78
- *
79
- * // In your SPA route handler:
80
- * const raw = new URLSearchParams(window.location.search).get("selection");
81
- * const { ids, gridType } = parseMassActionSelection(raw);
82
- * ```
83
- */
84
- declare function parseMassActionSelection(rawSelection: unknown): MassActionSelection;
85
- //#endregion
86
45
  //#region source/mass-actions/worker/schema.d.ts
87
46
  /**
88
47
  * Grid identifier sent by Commerce on the `commerce/backend-ui/2` wire contract
@@ -157,4 +116,4 @@ declare function okMassActionResponse(body?: MassActionResponseBody): SuccessRes
157
116
  */
158
117
  declare function massActionErrorResponse(statusCode: number, errorMessage: string): ErrorResponse<MassActionErrorBody>;
159
118
  //#endregion
160
- export { type AdminUiEntity, type MassActionErrorBody, type MassActionGridType, MassActionGridTypeSchema, type MassActionRequest, MassActionRequestSchema, type MassActionResponseBody, type MassActionSelection, MassActionSelectionSchema, getMassActionAclResourceId, massActionErrorResponse, okMassActionResponse, parseMassActionRequest, parseMassActionSelection };
119
+ export { type AdminUiEntity, type MassActionErrorBody, type MassActionGridType, MassActionGridTypeSchema, type MassActionRequest, MassActionRequestSchema, type MassActionResponseBody, getMassActionAclResourceId, massActionErrorResponse, okMassActionResponse, parseMassActionRequest };
@@ -53,6 +53,37 @@ function isControlFrame() {
53
53
  return isEmbeddedInHost() && !window.name;
54
54
  }
55
55
 
56
+ //#endregion
57
+ //#region source/web/react/result.ts
58
+ /** Creates a successful data result. */
59
+ function ok(data) {
60
+ return {
61
+ data,
62
+ error: null
63
+ };
64
+ }
65
+ /** Creates a failed data result. */
66
+ function error(message, options) {
67
+ return {
68
+ data: null,
69
+ error: new Error(message, options)
70
+ };
71
+ }
72
+ /** Creates a successful actions result. */
73
+ function okActions(actions) {
74
+ return {
75
+ actions,
76
+ error: null
77
+ };
78
+ }
79
+ /** Creates a failed actions result. */
80
+ function actionsError(message, options) {
81
+ return {
82
+ actions: null,
83
+ error: new Error(message, options)
84
+ };
85
+ }
86
+
56
87
  //#endregion
57
88
  //#region source/web/react/auth/context/ims-context.tsx
58
89
  const ImsContextValue = (0, react.createContext)(void 0);
@@ -60,14 +91,13 @@ const ImsContextValue = (0, react.createContext)(void 0);
60
91
  * Returns the IMS credentials provided by the host. Works inside the Commerce Admin and the
61
92
  * Experience Cloud shell.
62
93
  *
63
- * @throws If no host provides credentials (e.g. the app is running standalone, outside both the
64
- * Commerce Admin and the Experience Cloud shell).
94
+ * Returns an error when no host provides credentials.
65
95
  */
66
96
  function useIms() {
67
97
  const credentials = (0, react.use)(ImsContextValue);
68
- if (credentials === void 0) throw new Error("useIms must be used inside an ImsContextProvider.");
69
- if (!credentials) throw new Error("useIms requires running inside the Commerce Admin or the Experience Cloud shell, which provide the IMS credentials.");
70
- return credentials;
98
+ if (credentials === void 0) return error("useIms must be used inside an ImsContextProvider.");
99
+ if (!credentials) return error("useIms requires running inside the Commerce Admin or the Experience Cloud shell, which provide the IMS credentials.");
100
+ return ok(credentials);
71
101
  }
72
102
  /**
73
103
  * Provides the IMS credentials for a mounted Admin UI iframe app.
@@ -89,9 +119,7 @@ const SharedContextValue = (0, react.createContext)(void 0);
89
119
  * Returns the current Commerce shared context provider state.
90
120
  */
91
121
  function useInternalSharedContext() {
92
- const context = (0, react.use)(SharedContextValue);
93
- if (context === void 0) throw new Error("useSharedContext must be used inside a SharedContextProvider, which is only available in the Commerce Admin.");
94
- return context;
122
+ return (0, react.use)(SharedContextValue);
95
123
  }
96
124
  /**
97
125
  * Returns the current Commerce shared context. The guest connection is already established by
@@ -101,26 +129,29 @@ function useInternalSharedContext() {
101
129
  * Prefer a purpose-built hook ({@link useCommerce}, {@link useMassActionContext},
102
130
  * {@link useOrderViewButtonContext}) when one covers what you need.
103
131
  *
104
- * @throws If used outside a {@link SharedContextProvider}.
105
- *
106
132
  * @example
107
133
  * ```tsx
108
134
  * import { useSharedContext } from "@adobe/aio-commerce-lib-admin-ui/web";
109
135
  *
110
136
  * function ImsTokenLabel() {
111
- * const { sharedContext } = useSharedContext();
112
- * return <span>{sharedContext.get("imsToken")}</span>;
137
+ * const { data, error } = useSharedContext();
138
+ * if (error) return null;
139
+ * return <span>{data.sharedContext.get("imsToken")}</span>;
113
140
  * }
114
141
  * ```
115
142
  */
116
143
  function useSharedContext() {
117
- const { extensionId, guestConnection } = useInternalSharedContext();
118
- const sharedContext = useLiveSharedContext(guestConnection);
119
- return {
120
- extensionId,
121
- host: guestConnection.host,
122
- sharedContext
123
- };
144
+ const context = useInternalSharedContext();
145
+ const sharedContext = useLiveSharedContext(context?.guestConnection);
146
+ return (0, react.useMemo)(() => {
147
+ if (!context) return error("useSharedContext must be used inside a SharedContextProvider, which is only available in the Commerce Admin.");
148
+ if (!sharedContext) return error("The Commerce host did not provide a shared context for this extension.");
149
+ return ok({
150
+ extensionId: context.extensionId,
151
+ host: context.guestConnection.host,
152
+ sharedContext
153
+ });
154
+ }, [context, sharedContext]);
124
155
  }
125
156
  /**
126
157
  * Tracks the live UIX `sharedContext` for a connection.
@@ -132,7 +163,10 @@ function useSharedContext() {
132
163
  * @param guestConnection - The established guest connection.
133
164
  */
134
165
  function useLiveSharedContext(guestConnection) {
135
- return (0, react.useSyncExternalStore)((0, react.useCallback)((onContextChange) => guestConnection.addEventListener("contextchange", onContextChange), [guestConnection]), () => guestConnection.sharedContext);
166
+ return (0, react.useSyncExternalStore)((0, react.useCallback)((onContextChange) => {
167
+ if (!guestConnection) return () => void 0;
168
+ return guestConnection.addEventListener("contextchange", onContextChange);
169
+ }, [guestConnection]), () => guestConnection?.sharedContext ?? null);
136
170
  }
137
171
  /**
138
172
  * Provides the Commerce shared context for a mounted Admin UI iframe app.
@@ -154,28 +188,33 @@ function SharedContextProvider(props) {
154
188
  //#endregion
155
189
  //#region source/web/react/promise-cache.ts
156
190
  /**
157
- * Creates a keyed cache that memoizes in-flight, resolved, and rejected promises, giving `use()`
191
+ * Creates a keyed cache that memoizes in-flight, successful, and failed promises, giving `use()`
158
192
  * the reference-stable promise it needs while suspended and single-flighting side-effecting
159
193
  * establishment calls across re-renders, remounts, and StrictMode double-invocation.
160
194
  *
161
- * Rejections are retained (not evicted) so `use()` replays them to an error boundary instead of
162
- * suspending forever on a fresh pending promise. Retry a failed key via {@link RetryablePromiseCache.evict}.
195
+ * Promise rejections are failures by default. Pass `isFailure` when a fulfilled value, such as an
196
+ * error result, must also be retryable. Failed entries remain cached until `evictIfFailed` is
197
+ * called, so retries never replace a stable promise during render.
198
+ *
199
+ * @param isFailure - Determines whether a fulfilled value represents a failure.
163
200
  */
164
- function createRetryablePromiseCache() {
201
+ function createRetryablePromiseCache(isFailure = () => false) {
165
202
  const cache = /* @__PURE__ */ new Map();
166
203
  return {
167
- evictIfRejected(key) {
168
- if (cache.get(key)?.rejected) cache.delete(key);
204
+ evictIfFailed(key) {
205
+ if (cache.get(key)?.failed) cache.delete(key);
169
206
  },
170
207
  get(key, create) {
171
208
  const cached = cache.get(key);
172
209
  if (cached) return cached.promise;
173
210
  const entry = {
174
- promise: create(),
175
- rejected: false
211
+ failed: false,
212
+ promise: create()
176
213
  };
177
- entry.promise.catch(() => {
178
- entry.rejected = true;
214
+ entry.promise.then((value) => {
215
+ entry.failed = isFailure(value);
216
+ }).catch(() => {
217
+ entry.failed = true;
179
218
  });
180
219
  cache.set(key, entry);
181
220
  return entry.promise;
@@ -185,7 +224,7 @@ function createRetryablePromiseCache() {
185
224
 
186
225
  //#endregion
187
226
  //#region source/web/react/commerce/hooks/use-commerce.ts
188
- const commerceHosts = createRetryablePromiseCache();
227
+ const commerceHosts = createRetryablePromiseCache((result) => result.error !== null);
189
228
  /**
190
229
  * Returns the cached Commerce Admin host promise for an extension, resolving it once over the
191
230
  * given guest connection. The value is static for the lifetime of the connection.
@@ -194,24 +233,31 @@ const commerceHosts = createRetryablePromiseCache();
194
233
  * @param connection - The established guest connection.
195
234
  */
196
235
  function getCommerceHostPromise(extensionId, connection) {
197
- const { integration } = connection.host;
198
- if (!integration) throw new Error("The host does not provide the integration API needed to resolve the Commerce host.");
199
- return commerceHosts.get(extensionId, () => integration.getCommerceHost());
236
+ return commerceHosts.get(extensionId, async () => {
237
+ const { integration } = connection.host;
238
+ if (!integration) return error("The host does not provide the integration API needed to resolve the Commerce host.");
239
+ try {
240
+ return ok({ commerceHost: await integration.getCommerceHost() });
241
+ } catch (cause) {
242
+ return error("Failed to resolve the Commerce host.", { cause });
243
+ }
244
+ });
200
245
  }
201
246
  /** Drops a failed Commerce host resolution for `extensionId`, so a later render retries it. */
202
247
  function retryCommerceHost(extensionId) {
203
- commerceHosts.evictIfRejected(extensionId);
248
+ commerceHosts.evictIfFailed(extensionId);
204
249
  }
205
250
  /**
206
251
  * Returns the host (domain) of the Commerce Admin the extension is embedded in, resolving it over
207
252
  * the guest connection.
208
253
  *
209
- * @throws If used outside a Commerce Admin UI frame, or when the host does not expose the
210
- * Commerce integration API.
254
+ * Returns an error when used outside a Commerce Admin UI frame, when the host does not expose the
255
+ * Commerce integration API, or when resolving the host fails.
211
256
  */
212
257
  function useCommerce() {
213
- const { extensionId, guestConnection } = useInternalSharedContext();
214
- return { commerceHost: (0, react.use)(getCommerceHostPromise(extensionId, guestConnection)) };
258
+ const context = useInternalSharedContext();
259
+ if (!context) return error("useCommerce requires running inside the Commerce Admin.");
260
+ return (0, react.use)(getCommerceHostPromise(context.extensionId, context.guestConnection));
215
261
  }
216
262
 
217
263
  //#endregion
@@ -220,28 +266,31 @@ function useCommerce() {
220
266
  * Returns the context for a mass-action extension point: the selected row IDs the action was
221
267
  * triggered with. The value is read from the host-provided Commerce context.
222
268
  *
223
- * @throws If used outside the Commerce shared context, or when that context does not include a
224
- * mass-action selection.
269
+ * Returns an error outside the Commerce shared context, or when the mass-action selection is
270
+ * missing, empty, or contains a non-string row ID.
225
271
  */
226
272
  function useMassActionContext() {
227
- const { sharedContext } = useSharedContext();
273
+ const { data, error: contextError } = useSharedContext();
228
274
  return (0, react.useMemo)(() => {
229
- const selectedIds = sharedContext.get("selectedIds");
230
- 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?");
231
- return { selectedIds };
232
- }, [sharedContext]);
275
+ if (contextError) return error(contextError.message, { cause: contextError });
276
+ const selectedIds = data.sharedContext.get("selectedIds");
277
+ 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?");
278
+ if (selectedIds.length === 0) return error("No rows selected. A mass-action extension point must be triggered with at least one selected row.");
279
+ 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.");
280
+ return ok({ selectedIds });
281
+ }, [data, contextError]);
233
282
  }
234
283
  /**
235
284
  * Returns the context for an order view-button extension point: the order ID the button was
236
285
  * triggered from.
237
286
  *
238
- * @throws If no order ID is present in the page URL.
287
+ * Returns an error when no order ID is present in the page URL.
239
288
  */
240
289
  function useOrderViewButtonContext() {
241
290
  return (0, react.useMemo)(() => {
242
291
  const orderId = parseOrderId(globalThis.location.href);
243
- if (orderId === null) throw new Error("Could not find an order ID. Is this frame running as an order view-button extension point?");
244
- return { orderId };
292
+ if (orderId === null) return error("Could not find an order ID. Is this frame running as an order view-button extension point?");
293
+ return ok({ orderId });
245
294
  }, []);
246
295
  }
247
296
 
@@ -250,51 +299,39 @@ function useOrderViewButtonContext() {
250
299
  /**
251
300
  * Returns typed helpers for interacting with the Commerce Admin host.
252
301
  *
253
- * @throws If called before the guest connection is established, or when the host frame actions
254
- * are unavailable.
255
- *
256
302
  * @example
257
303
  * ```tsx
258
304
  * import { useHostConnection } from "@adobe/aio-commerce-lib-admin-ui/web";
259
305
  *
260
306
  * function DoneButton() {
261
- * const { close } = useHostConnection();
262
- * return <button onClick={() => void close()}>Done</button>;
307
+ * const { actions, error } = useHostConnection();
308
+ * if (error) return null;
309
+ * return <button onClick={actions.close}>Done</button>;
263
310
  * }
264
311
  * ```
265
312
  */
266
313
  function useHostConnection() {
267
- const { host } = useSharedContext();
314
+ const { data, error: contextError } = useSharedContext();
268
315
  return (0, react.useMemo)(() => {
269
- const { field } = host;
270
- const requireField = () => {
271
- if (!field) throw new Error("Host frame actions are unavailable. They require an established guest connection with a host that exposes frame actions.");
272
- return field;
273
- };
274
- return {
275
- close: () => requireField().close(),
276
- closeWithError: () => requireField().onError()
277
- };
278
- }, [host]);
316
+ if (contextError) return actionsError(contextError.message, { cause: contextError });
317
+ const { field } = data.host;
318
+ if (!field) return actionsError("Host frame actions are unavailable. They require an established guest connection with a host that exposes frame actions.");
319
+ return okActions({
320
+ close: () => field.close(),
321
+ closeWithError: () => field.onError()
322
+ });
323
+ }, [data, contextError]);
279
324
  }
280
325
 
281
326
  //#endregion
282
327
  //#region source/web/react/routing/lib.ts
283
328
  const HASH_ROUTE_PREFIX_PATTERN = /^[#/]+/u;
284
329
  /**
285
- * Determines if a given route is an index route.
286
- * @param route The route to check.
287
- */
288
- function isIndexRoute(route) {
289
- return "index" in route && route.index;
290
- }
291
- /**
292
330
  * Returns the path for a given route, removing any leading hash or slash characters.
293
331
  * @param route The route to get the path for.
294
332
  */
295
333
  function getRoutePath(route) {
296
- if (isIndexRoute(route)) return "/";
297
- return route.path.replace(HASH_ROUTE_PREFIX_PATTERN, "");
334
+ return route.path.replace(HASH_ROUTE_PREFIX_PATTERN, "") || "/";
298
335
  }
299
336
  /**
300
337
  * Returns the "to" path for a given route, removing any leading hash or slash characters.
@@ -436,7 +473,7 @@ function useGuestConnection(extensionId) {
436
473
  }
437
474
  /** Drops a failed connection for `extensionId`, so a later render re-attaches. */
438
475
  function retryGuestConnection(extensionId) {
439
- guestConnections.evictIfRejected(extensionId);
476
+ guestConnections.evictIfFailed(extensionId);
440
477
  }
441
478
 
442
479
  //#endregion
@@ -627,9 +664,10 @@ function CommerceGuestConnection(props) {
627
664
  }
628
665
  /** Renders IMS-gated route content for a Commerce Admin UI frame. */
629
666
  function CommerceExtensionContent() {
630
- const { sharedContext } = useSharedContext();
667
+ const { data, error } = useSharedContext();
668
+ if (error) throw error;
631
669
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImsContextProvider, {
632
- credentials: resolveCommerceImsCredentials(sharedContext),
670
+ credentials: resolveCommerceImsCredentials(data.sharedContext),
633
671
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_tanstack_react_router.Outlet, {})
634
672
  });
635
673
  }
@@ -785,11 +823,12 @@ function Entrypoint(props) {
785
823
  *
786
824
  * createExtensionApp({
787
825
  * metadata: { extensionId: "my-extension-id" },
788
- * routes: [{ index: true, element: <MainPage /> }],
826
+ * menu: <MainPage />,
789
827
  * });
790
828
  * ```
791
829
  */
792
- function createExtensionApp({ metadata, routes, root: customRoot }) {
830
+ function createExtensionApp({ menu, metadata, routes = [], root: customRoot }) {
831
+ 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.");
793
832
  const rootElement = customRoot ?? document.getElementById("root");
794
833
  if (!rootElement) throw new Error("Could not find an element with id \"root\".");
795
834
  const root = (0, react_dom_client.createRoot)(rootElement);
@@ -798,7 +837,10 @@ function createExtensionApp({ metadata, routes, root: customRoot }) {
798
837
  extensionId: metadata.extensionId,
799
838
  initialConfigurationPromise,
800
839
  runtime
801
- }), routes);
840
+ }), menu === void 0 ? routes : [{
841
+ element: menu,
842
+ path: "/"
843
+ }, ...routes]);
802
844
  root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.StrictMode, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_tanstack_react_router.RouterProvider, { router }) }));
803
845
  };
804
846
  try {