@adobe/aio-commerce-lib-admin-ui 0.1.0 → 0.2.0-alpha-20260722091448

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/dist/cjs/acl-resource-id-DBlYU0DE.d.cts +39 -0
  4. package/dist/cjs/acl-resource-id-D_hCU1Qg.cjs +69 -0
  5. package/dist/cjs/api/index.cjs +252 -0
  6. package/dist/cjs/api/index.d.cts +126 -0
  7. package/dist/cjs/grid-columns/index.cjs +151 -0
  8. package/dist/cjs/grid-columns/index.d.cts +146 -0
  9. package/dist/cjs/mass-actions/index.cjs +188 -0
  10. package/dist/cjs/mass-actions/index.d.cts +160 -0
  11. package/dist/cjs/menu/index.cjs +91 -0
  12. package/dist/cjs/menu/index.d.cts +63 -0
  13. package/dist/cjs/order-view-buttons/index.cjs +126 -0
  14. package/dist/cjs/order-view-buttons/index.d.cts +113 -0
  15. package/dist/cjs/rolldown-runtime-Cx6hovH8.cjs +67 -0
  16. package/dist/cjs/schemas-Ce10uBzN.cjs +41 -0
  17. package/dist/cjs/utils-B59fjd_w.cjs +39 -0
  18. package/dist/cjs/web/index.cjs +872 -0
  19. package/dist/cjs/web/index.d.cts +206 -0
  20. package/dist/es/acl-resource-id-DBlYU0DE.d.mts +39 -0
  21. package/dist/es/acl-resource-id-pryVxI_c.mjs +57 -0
  22. package/dist/es/api/index.d.mts +126 -0
  23. package/dist/es/api/index.mjs +262 -0
  24. package/dist/es/grid-columns/index.d.mts +146 -0
  25. package/dist/es/grid-columns/index.mjs +143 -0
  26. package/dist/es/mass-actions/index.d.mts +160 -0
  27. package/dist/es/mass-actions/index.mjs +178 -0
  28. package/dist/es/menu/index.d.mts +63 -0
  29. package/dist/es/menu/index.mjs +80 -0
  30. package/dist/es/order-view-buttons/index.d.mts +113 -0
  31. package/dist/es/order-view-buttons/index.mjs +119 -0
  32. package/dist/es/schemas-BFT8ys8P.mjs +34 -0
  33. package/dist/es/utils-COPGW1HO.mjs +32 -0
  34. package/dist/es/web/index.d.mts +206 -0
  35. package/dist/es/web/index.mjs +861 -0
  36. package/package.json +87 -10
  37. package/dist/cjs/index.cjs +0 -139
  38. package/dist/cjs/index.d.cts +0 -56
  39. package/dist/es/index.d.mts +0 -56
  40. package/dist/es/index.mjs +0 -114
@@ -0,0 +1,861 @@
1
+ /**
2
+ * @license
3
+ *
4
+ * Copyright 2026 Adobe. All rights reserved.
5
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License. You may obtain a copy
7
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under
10
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
11
+ * OF ANY KIND, either express or implied. See the License for the specific language
12
+ * governing permissions and limitations under the License.
13
+ */
14
+
15
+ import { StrictMode, Suspense, createContext, use, useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
16
+ import { jsx, jsxs } from "react/jsx-runtime";
17
+ import Runtime, { init } from "@adobe/exc-app";
18
+ import page from "@adobe/exc-app/page.js";
19
+ import { Outlet, RouterProvider, createHashHistory, createRootRoute, createRoute, createRouter, useCanGoBack, useRouter, useRouterState } from "@tanstack/react-router";
20
+ import { createRoot } from "react-dom/client";
21
+ import { attach, register } from "@adobe/uix-guest";
22
+ import { Provider } from "@react-spectrum/s2/Provider";
23
+ import { ProgressCircle } from "@react-spectrum/s2/ProgressCircle";
24
+ import { Button, ButtonGroup } from "@react-spectrum/s2/ButtonGroup";
25
+ import { Content, Heading, IllustratedMessage } from "@react-spectrum/s2/IllustratedMessage";
26
+ import ErrorIllustration from "@react-spectrum/s2/illustrations/linear/Error";
27
+ import { ErrorBoundary } from "react-error-boundary";
28
+
29
+ //#region source/web/react/commerce/lib.ts
30
+ /**
31
+ * Extracts the order ID from the given URL (must be absolute).
32
+ * @param href - The URL to read the order ID from (typically `window.location.href`).
33
+ */
34
+ function parseOrderId(href) {
35
+ const urlObj = new URL(href);
36
+ return urlObj.searchParams.get("orderId") ?? new URLSearchParams(urlObj.hash.split("?")[1]).get("orderId");
37
+ }
38
+ /** Whether the app is embedded in a host frame (the Commerce Admin or the Experience Cloud shell). */
39
+ function isEmbeddedInHost() {
40
+ return globalThis.window.parent !== globalThis.window;
41
+ }
42
+ /** Whether this window is a Commerce UIX guest UI frame, as opposed to a control frame or standalone. */
43
+ function isUiFrame() {
44
+ return isEmbeddedInHost() && window.name.startsWith("uix-guest-");
45
+ }
46
+ /** Whether this window is a Commerce UIX guest control frame, which registers instead of attaching. */
47
+ function isControlFrame() {
48
+ return isEmbeddedInHost() && !window.name;
49
+ }
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
+
82
+ //#endregion
83
+ //#region source/web/react/auth/context/ims-context.tsx
84
+ const ImsContextValue = createContext(void 0);
85
+ /**
86
+ * Returns the IMS credentials provided by the host. Works inside the Commerce Admin and the
87
+ * Experience Cloud shell.
88
+ *
89
+ * Returns an error when no host provides credentials.
90
+ */
91
+ function useIms() {
92
+ const credentials = use(ImsContextValue);
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);
96
+ }
97
+ /**
98
+ * Provides the IMS credentials for a mounted Admin UI iframe app.
99
+ * @param props - The resolved credentials (or null while unavailable) and the children to render.
100
+ */
101
+ function ImsContextProvider(props) {
102
+ const { children, credentials } = props;
103
+ if (isEmbeddedInHost() && !credentials) return null;
104
+ return /* @__PURE__ */ jsx(ImsContextValue.Provider, {
105
+ value: credentials,
106
+ children
107
+ });
108
+ }
109
+
110
+ //#endregion
111
+ //#region source/web/react/commerce/context/shared-context.tsx
112
+ const SharedContextValue = createContext(void 0);
113
+ /**
114
+ * Returns the current Commerce shared context provider state.
115
+ */
116
+ function useInternalSharedContext() {
117
+ return use(SharedContextValue);
118
+ }
119
+ /**
120
+ * Returns the current Commerce shared context. The guest connection is already established by
121
+ * the time this can be called (see {@link SharedContextProvider}).
122
+ *
123
+ * This is a low-level escape hatch that exposes the raw `sharedContext` and `host` objects.
124
+ * Prefer a purpose-built hook ({@link useCommerce}, {@link useMassActionContext},
125
+ * {@link useOrderViewButtonContext}) when one covers what you need.
126
+ *
127
+ * @example
128
+ * ```tsx
129
+ * import { useSharedContext } from "@adobe/aio-commerce-lib-admin-ui/web";
130
+ *
131
+ * function ImsTokenLabel() {
132
+ * const { data, error } = useSharedContext();
133
+ * if (error) return null;
134
+ * return <span>{data.sharedContext.get("imsToken")}</span>;
135
+ * }
136
+ * ```
137
+ */
138
+ function useSharedContext() {
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]);
150
+ }
151
+ /**
152
+ * Tracks the live UIX `sharedContext` for a connection.
153
+ *
154
+ * The host shares `sharedContext` on connect and may reassign it on later `contextchange`
155
+ * events. `useSyncExternalStore` subscribes to those events and surfaces the current instance
156
+ * (whose reference changes on each update), so consumers re-render when the host updates it.
157
+ *
158
+ * @param guestConnection - The established guest connection.
159
+ */
160
+ function useLiveSharedContext(guestConnection) {
161
+ return useSyncExternalStore(useCallback((onContextChange) => {
162
+ if (!guestConnection) return () => void 0;
163
+ return guestConnection.addEventListener("contextchange", onContextChange);
164
+ }, [guestConnection]), () => guestConnection?.sharedContext ?? null);
165
+ }
166
+ /**
167
+ * Provides the Commerce shared context for a mounted Admin UI iframe app.
168
+ * @param props - The props needed to initialize the shared context, including the already
169
+ * established guest connection.
170
+ */
171
+ function SharedContextProvider(props) {
172
+ const { children, guestConnection, extensionId } = props;
173
+ const value = useMemo(() => ({
174
+ extensionId,
175
+ guestConnection
176
+ }), [extensionId, guestConnection]);
177
+ return /* @__PURE__ */ jsx(SharedContextValue.Provider, {
178
+ value,
179
+ children
180
+ });
181
+ }
182
+
183
+ //#endregion
184
+ //#region source/web/react/promise-cache.ts
185
+ /**
186
+ * Creates a keyed cache that memoizes in-flight, successful, and failed promises, giving `use()`
187
+ * the reference-stable promise it needs while suspended and single-flighting side-effecting
188
+ * establishment calls across re-renders, remounts, and StrictMode double-invocation.
189
+ *
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.
195
+ */
196
+ function createRetryablePromiseCache(isFailure = () => false) {
197
+ const cache = /* @__PURE__ */ new Map();
198
+ return {
199
+ evictIfFailed(key) {
200
+ if (cache.get(key)?.failed) cache.delete(key);
201
+ },
202
+ get(key, create) {
203
+ const cached = cache.get(key);
204
+ if (cached) return cached.promise;
205
+ const entry = {
206
+ failed: false,
207
+ promise: create()
208
+ };
209
+ entry.promise.then((value) => {
210
+ entry.failed = isFailure(value);
211
+ }).catch(() => {
212
+ entry.failed = true;
213
+ });
214
+ cache.set(key, entry);
215
+ return entry.promise;
216
+ }
217
+ };
218
+ }
219
+
220
+ //#endregion
221
+ //#region source/web/react/commerce/hooks/use-commerce.ts
222
+ const commerceHosts = createRetryablePromiseCache((result) => result.error !== null);
223
+ /**
224
+ * Returns the cached Commerce Admin host promise for an extension, resolving it once over the
225
+ * given guest connection. The value is static for the lifetime of the connection.
226
+ *
227
+ * @param extensionId - The unique identifier for the extension app.
228
+ * @param connection - The established guest connection.
229
+ */
230
+ function getCommerceHostPromise(extensionId, connection) {
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
+ });
240
+ }
241
+ /** Drops a failed Commerce host resolution for `extensionId`, so a later render retries it. */
242
+ function retryCommerceHost(extensionId) {
243
+ commerceHosts.evictIfFailed(extensionId);
244
+ }
245
+ /**
246
+ * Returns the host (domain) of the Commerce Admin the extension is embedded in, resolving it over
247
+ * the guest connection.
248
+ *
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.
251
+ */
252
+ function useCommerce() {
253
+ const context = useInternalSharedContext();
254
+ if (!context) return error("useCommerce requires running inside the Commerce Admin.");
255
+ return use(getCommerceHostPromise(context.extensionId, context.guestConnection));
256
+ }
257
+
258
+ //#endregion
259
+ //#region source/web/react/commerce/hooks/use-extension-context.ts
260
+ /**
261
+ * Returns the context for a mass-action extension point: the selected row IDs the action was
262
+ * triggered with. The value is read from the host-provided Commerce context.
263
+ *
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.
266
+ */
267
+ function useMassActionContext() {
268
+ const { data, error: contextError } = useSharedContext();
269
+ return useMemo(() => {
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]);
277
+ }
278
+ /**
279
+ * Returns the context for an order view-button extension point: the order ID the button was
280
+ * triggered from.
281
+ *
282
+ * Returns an error when no order ID is present in the page URL.
283
+ */
284
+ function useOrderViewButtonContext() {
285
+ return useMemo(() => {
286
+ const orderId = parseOrderId(globalThis.location.href);
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 });
289
+ }, []);
290
+ }
291
+
292
+ //#endregion
293
+ //#region source/web/react/commerce/hooks/use-host-connection.ts
294
+ /**
295
+ * Returns typed helpers for interacting with the Commerce Admin host.
296
+ *
297
+ * @example
298
+ * ```tsx
299
+ * import { useHostConnection } from "@adobe/aio-commerce-lib-admin-ui/web";
300
+ *
301
+ * function DoneButton() {
302
+ * const { actions, error } = useHostConnection();
303
+ * if (error) return null;
304
+ * return <button onClick={actions.close}>Done</button>;
305
+ * }
306
+ * ```
307
+ */
308
+ function useHostConnection() {
309
+ const { data, error: contextError } = useSharedContext();
310
+ return useMemo(() => {
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]);
319
+ }
320
+
321
+ //#endregion
322
+ //#region source/web/react/routing/lib.ts
323
+ const HASH_ROUTE_PREFIX_PATTERN = /^[#/]+/u;
324
+ /**
325
+ * Returns the path for a given route, removing any leading hash or slash characters.
326
+ * @param route The route to get the path for.
327
+ */
328
+ function getRoutePath(route) {
329
+ return route.path.replace(HASH_ROUTE_PREFIX_PATTERN, "") || "/";
330
+ }
331
+ /**
332
+ * Returns the "to" path for a given route, removing any leading hash or slash characters.
333
+ * @param path The path to convert to a "to" path.
334
+ */
335
+ function getRouteTo(path) {
336
+ const routePath = path.replace(HASH_ROUTE_PREFIX_PATTERN, "");
337
+ return routePath ? `/${routePath}` : "/";
338
+ }
339
+ /**
340
+ * Returns a router instance for an extension app, given the root component and route entries.
341
+ *
342
+ * @param rootComponent The root component of the extension app.
343
+ * @param routeEntries The route entries for the extension app.
344
+ * @param history The history implementation to use. Defaults to hash history; pass a memory
345
+ * history to get deterministic, seedable navigation (e.g. in tests).
346
+ */
347
+ function createExtensionRouter(rootComponent, routeEntries, history = createHashHistory()) {
348
+ const rootRoute = createRootRoute({ component: () => rootComponent });
349
+ const routes = routeEntries.map((route) => createRoute({
350
+ component: () => route.element,
351
+ getParentRoute: () => rootRoute,
352
+ path: getRoutePath(route)
353
+ }));
354
+ return createRouter({
355
+ history,
356
+ routeTree: rootRoute.addChildren(routes)
357
+ });
358
+ }
359
+
360
+ //#endregion
361
+ //#region source/web/runtime-loader.ts
362
+ const EXPERIENCE_CLOUD_RUNTIME_HOST_PATTERN = /^(exc-unifiedcontent\.)?experience(-qa|-stage|-cdn|-cdn-stage)?\.adobe\.(com|net)$/u;
363
+ /** Convenience no-operation function */
364
+ const noop = () => void 0;
365
+ /** Creates a mock implementation of the Experience Cloud runtime to be used when the real runtime is not available. */
366
+ function createMockRuntime() {
367
+ return {
368
+ configured: false,
369
+ emit: noop,
370
+ lastConfigurationPayload: null,
371
+ off: noop,
372
+ on: noop
373
+ };
374
+ }
375
+ /** Retrieve the URL of the Experience Cloud runtime script from the current page's query parameters or session storage. */
376
+ function getRuntimeScriptUrl() {
377
+ return new URL(globalThis.location.href).searchParams.get("_mr") || globalThis.sessionStorage.getItem("unifiedShellMRScript");
378
+ }
379
+ /**
380
+ * Loads the Experience Cloud runtime script into the current document.
381
+ * @throws {Error} If the runtime script cannot be loaded due to a missing or invalid script URL.
382
+ */
383
+ function loadExperienceCloudRuntime() {
384
+ const runtimeScriptUrl = getRuntimeScriptUrl();
385
+ if (!runtimeScriptUrl) throw new Error("Module Runtime: Missing script.");
386
+ const url = new URL(decodeURIComponent(runtimeScriptUrl));
387
+ if (url.protocol !== "https:") throw new Error("Module Runtime: Must be HTTPS.");
388
+ if (!(EXPERIENCE_CLOUD_RUNTIME_HOST_PATTERN.test(url.hostname) || url.hostname === "localhost.corp.adobe.com" || url.hostname.endsWith(".localhost.corp.adobe.com"))) throw new Error("Module Runtime: Invalid domain.");
389
+ if (!url.pathname.endsWith(".js")) throw new Error("Module Runtime: Must be a JavaScript file.");
390
+ globalThis.sessionStorage.setItem("unifiedShellMRScript", url.toString());
391
+ const script = document.createElement("script");
392
+ script.async = true;
393
+ script.src = url.toString();
394
+ script.onload = () => {
395
+ if ("EXC_MR_READY" in globalThis && typeof globalThis.EXC_MR_READY === "function") globalThis.EXC_MR_READY();
396
+ };
397
+ document.head.append(script);
398
+ }
399
+
400
+ //#endregion
401
+ //#region source/web/react/auth/lib.ts
402
+ /**
403
+ * Resolves IMS credentials from the Commerce shared context.
404
+ * @param sharedContext - The Commerce shared context, established over the guest connection.
405
+ */
406
+ function resolveCommerceImsCredentials(sharedContext) {
407
+ const imsToken = sharedContext.get("imsToken");
408
+ const imsOrgId = sharedContext.get("imsOrgId");
409
+ if (!(imsToken && imsOrgId)) return null;
410
+ return {
411
+ imsOrgId,
412
+ imsToken
413
+ };
414
+ }
415
+ /**
416
+ * Resolves IMS credentials from the Experience Cloud shell configuration.
417
+ * @param shellConfiguration - The Experience Cloud shell configuration, if available.
418
+ */
419
+ function resolveShellImsCredentials(shellConfiguration) {
420
+ if (!shellConfiguration) return null;
421
+ const { imsToken, imsOrg: imsOrgId } = shellConfiguration;
422
+ return {
423
+ imsOrgId,
424
+ imsToken
425
+ };
426
+ }
427
+
428
+ //#endregion
429
+ //#region source/web/react/centered-progress.tsx
430
+ /**
431
+ * A centered, indeterminate progress indicator used as a Suspense fallback.
432
+ * @param props - The props needed to render the progress indicator.
433
+ */
434
+ function CenteredProgress(props) {
435
+ return /* @__PURE__ */ jsx("div", {
436
+ style: {
437
+ alignItems: "center",
438
+ display: "flex",
439
+ justifyContent: "center",
440
+ minHeight: "calc(100dvh - 48px)",
441
+ padding: 24
442
+ },
443
+ children: /* @__PURE__ */ jsx(ProgressCircle, {
444
+ "aria-label": props["aria-label"],
445
+ isIndeterminate: true
446
+ })
447
+ });
448
+ }
449
+
450
+ //#endregion
451
+ //#region source/web/react/commerce/hooks/use-guest-connection.ts
452
+ const guestConnections = createRetryablePromiseCache();
453
+ function getGuestConnectionPromise(extensionId) {
454
+ return guestConnections.get(extensionId, () => {
455
+ const promise = attach({ id: extensionId });
456
+ promise.catch((err) => {
457
+ console.error("UIX guest attach failed:", err);
458
+ });
459
+ return promise;
460
+ });
461
+ }
462
+ /**
463
+ * Suspends until the guest connection for the given extension is established.
464
+ * @param extensionId - The unique identifier for the extension app.
465
+ */
466
+ function useGuestConnection(extensionId) {
467
+ return use(getGuestConnectionPromise(extensionId));
468
+ }
469
+ /** Drops a failed connection for `extensionId`, so a later render re-attaches. */
470
+ function retryGuestConnection(extensionId) {
471
+ guestConnections.evictIfFailed(extensionId);
472
+ }
473
+
474
+ //#endregion
475
+ //#region source/web/react/routing/hooks/use-spectrum-router.ts
476
+ /**
477
+ * Sets up a router instance for an extension app, based on the TanStack React Router example of React Spectrum.
478
+ * @see https://react-spectrum.adobe.com/getting-started
479
+ */
480
+ function useSpectrumRouter() {
481
+ const router = useRouter();
482
+ return useMemo(() => ({
483
+ navigate: (href, options) => {
484
+ if (typeof href === "string") return router.navigate({
485
+ to: getRouteTo(href),
486
+ ...options
487
+ });
488
+ return router.navigate({
489
+ ...href,
490
+ ...options
491
+ });
492
+ },
493
+ useHref: (href) => {
494
+ const toOptions = typeof href === "string" ? { to: getRouteTo(href) } : href;
495
+ return router.buildLocation(toOptions).href;
496
+ }
497
+ }), [router]);
498
+ }
499
+
500
+ //#endregion
501
+ //#region source/web/react/theme.ts
502
+ /**
503
+ * Maps host theme values to Spectrum S2 color schemes.
504
+ * @param theme - The theme value provided by the host runtime.
505
+ */
506
+ function getShellColorScheme(theme) {
507
+ switch (theme) {
508
+ case "dark":
509
+ case "spectrum--darkest": return "dark";
510
+ case "light":
511
+ case "spectrum--lightest": return "light";
512
+ default: return;
513
+ }
514
+ }
515
+ /**
516
+ * Syncs the root `data-color-scheme` attribute used by Spectrum S2 page styles.
517
+ *
518
+ * @param colorScheme - The Spectrum S2 color scheme to apply.
519
+ */
520
+ function syncRootColorScheme(colorScheme) {
521
+ if (!colorScheme) {
522
+ delete document.documentElement.dataset.colorScheme;
523
+ return;
524
+ }
525
+ document.documentElement.dataset.colorScheme = colorScheme;
526
+ }
527
+
528
+ //#endregion
529
+ //#region source/web/react/extension/error-boundary.tsx
530
+ function getErrorMessage(error) {
531
+ if (error instanceof Error) return error.message;
532
+ return typeof error === "string" ? error : "An unexpected error occurred.";
533
+ }
534
+ function ErrorFallback({ error, resetErrorBoundary }) {
535
+ const router = useRouter();
536
+ const canGoBack = useCanGoBack();
537
+ const goBack = useCallback(() => {
538
+ router.history.back();
539
+ }, [router]);
540
+ return /* @__PURE__ */ jsx("div", {
541
+ style: {
542
+ alignItems: "center",
543
+ display: "flex",
544
+ justifyContent: "center",
545
+ minHeight: "calc(100dvh - 48px)",
546
+ padding: 24
547
+ },
548
+ children: /* @__PURE__ */ jsxs(IllustratedMessage, { children: [
549
+ /* @__PURE__ */ jsx(ErrorIllustration, {}),
550
+ /* @__PURE__ */ jsx(Heading, { children: "Something went wrong" }),
551
+ /* @__PURE__ */ jsx(Content, { children: getErrorMessage(error) }),
552
+ /* @__PURE__ */ jsxs(ButtonGroup, { children: [canGoBack && /* @__PURE__ */ jsx(Button, {
553
+ onPress: goBack,
554
+ variant: "secondary",
555
+ children: "Go back"
556
+ }), /* @__PURE__ */ jsx(Button, {
557
+ onPress: resetErrorBoundary,
558
+ variant: "accent",
559
+ children: "Try again"
560
+ })] })
561
+ ] })
562
+ });
563
+ }
564
+ /**
565
+ * A wrapper component that provides an error boundary for extension apps.
566
+ * It catches JavaScript errors anywhere in its child component tree, logs those errors, and displays a fallback UI.
567
+ *
568
+ * @param props - The props for the component, including children elements and an optional
569
+ * `onReset` callback run before the boundary re-renders its children (via "Try again" or a
570
+ * route change), e.g. to drop cached failures so a retry starts fresh.
571
+ */
572
+ function ExtensionErrorBoundary(props) {
573
+ const routeKey = useRouterState({ select: (state) => state.matches.at(-1)?.pathname });
574
+ return /* @__PURE__ */ jsx(ErrorBoundary, {
575
+ FallbackComponent: ErrorFallback,
576
+ onReset: props.onReset,
577
+ resetKeys: [routeKey],
578
+ children: props.children
579
+ });
580
+ }
581
+
582
+ //#endregion
583
+ //#region source/web/react/extension/commerce-app.tsx
584
+ const controlFrameRegistrations = createRetryablePromiseCache();
585
+ /**
586
+ * Registers a Commerce Admin control frame with the UIX host, once per extension. `register`
587
+ * has no teardown, so this is memoized rather than run inside an effect body every mount.
588
+ *
589
+ * @param extensionId - The unique identifier for the extension app.
590
+ */
591
+ function registerControlFrame(extensionId) {
592
+ return controlFrameRegistrations.get(extensionId, () => {
593
+ const promise = register({
594
+ id: extensionId,
595
+ methods: {}
596
+ });
597
+ promise.catch((err) => {
598
+ console.error("UIX guest register failed:", err);
599
+ });
600
+ return promise;
601
+ });
602
+ }
603
+ /** The fallback UI shown when connecting to the Commerce host. */
604
+ function ConnectionFallback() {
605
+ return /* @__PURE__ */ jsx(CenteredProgress, { "aria-label": "Connecting to Commerce Admin" });
606
+ }
607
+ /**
608
+ * Renders the Commerce Admin flow: a control frame only needs to register itself with the UIX
609
+ * host (no visible content), while a UI frame renders the routed extension-point content once
610
+ * its guest connection is established.
611
+ *
612
+ * @param props - The props needed to initialize the extension app.
613
+ */
614
+ function CommerceExtensionApp(props) {
615
+ const { extensionId } = props;
616
+ const spectrumRouter = useSpectrumRouter();
617
+ const resetConnections = useCallback(() => {
618
+ retryGuestConnection(extensionId);
619
+ retryCommerceHost(extensionId);
620
+ }, [extensionId]);
621
+ useEffect(() => {
622
+ syncRootColorScheme("light");
623
+ }, []);
624
+ if (isControlFrame()) return /* @__PURE__ */ jsx(ControlFrameRegistration, { extensionId });
625
+ return /* @__PURE__ */ jsx(Provider, {
626
+ colorScheme: "light",
627
+ router: spectrumRouter,
628
+ children: /* @__PURE__ */ jsx(ExtensionErrorBoundary, {
629
+ onReset: resetConnections,
630
+ children: /* @__PURE__ */ jsx(Suspense, {
631
+ fallback: /* @__PURE__ */ jsx(ConnectionFallback, {}),
632
+ children: /* @__PURE__ */ jsx(CommerceGuestConnection, { extensionId })
633
+ })
634
+ })
635
+ });
636
+ }
637
+ /**
638
+ * Registers a Commerce Admin control frame with the UIX host. Renders no visible content.
639
+ * @param props - The props needed to initialize the extension app.
640
+ */
641
+ function ControlFrameRegistration(props) {
642
+ const { extensionId } = props;
643
+ useEffect(() => {
644
+ registerControlFrame(extensionId);
645
+ }, [extensionId]);
646
+ return null;
647
+ }
648
+ /**
649
+ * Suspends until the guest connection is established, then provides the Commerce shared context.
650
+ * @param props - The props needed to initialize the extension app.
651
+ */
652
+ function CommerceGuestConnection(props) {
653
+ const { extensionId } = props;
654
+ return /* @__PURE__ */ jsx(SharedContextProvider, {
655
+ extensionId,
656
+ guestConnection: useGuestConnection(extensionId),
657
+ children: /* @__PURE__ */ jsx(CommerceExtensionContent, {})
658
+ });
659
+ }
660
+ /** Renders IMS-gated route content for a Commerce Admin UI frame. */
661
+ function CommerceExtensionContent() {
662
+ const { data, error } = useSharedContext();
663
+ if (error) throw error;
664
+ return /* @__PURE__ */ jsx(ImsContextProvider, {
665
+ credentials: resolveCommerceImsCredentials(data.sharedContext),
666
+ children: /* @__PURE__ */ jsx(Outlet, {})
667
+ });
668
+ }
669
+
670
+ //#endregion
671
+ //#region source/web/react/shell/hooks/use-extension-color-scheme.ts
672
+ /**
673
+ * Keeps the Spectrum S2 color scheme aligned with Experience Shell or browser defaults.
674
+ * @param shellConfiguration - The Experience Shell configuration, if available.
675
+ */
676
+ function useExtensionColorScheme(shellConfiguration) {
677
+ const colorScheme = getShellColorScheme(shellConfiguration?.theme);
678
+ useEffect(() => {
679
+ syncRootColorScheme(colorScheme);
680
+ }, [colorScheme]);
681
+ return colorScheme;
682
+ }
683
+
684
+ //#endregion
685
+ //#region source/web/react/shell/hooks/use-shell-configuration.ts
686
+ /**
687
+ * Extracts the shell configuration from the runtime configuration (only the components we want to expose).
688
+ * @param config The runtime configuration object.
689
+ */
690
+ function extractShellConfiguration(config) {
691
+ const { imsOrg, imsToken, theme } = config;
692
+ return {
693
+ imsOrg,
694
+ imsToken,
695
+ theme
696
+ };
697
+ }
698
+ /**
699
+ * Derives the exposed shell configuration from the given runtime instance.
700
+ * @param runtime The runtime instance.
701
+ */
702
+ function useShellConfiguration(runtime, initialConfiguration) {
703
+ const router = useRouter();
704
+ const [shellConfiguration, setShellConfiguration] = useState(() => initialConfiguration ? extractShellConfiguration(initialConfiguration) : null);
705
+ useEffect(() => {
706
+ const onConfiguration = (event) => {
707
+ if (event) setShellConfiguration(extractShellConfiguration(event));
708
+ };
709
+ const onHistory = (event) => {
710
+ if (event?.type === "external" && typeof event.path === "string") {
711
+ const to = getRouteTo(event.path);
712
+ if (to !== router.state.location.pathname) router.navigate({ to });
713
+ }
714
+ };
715
+ runtime.on("configuration", onConfiguration);
716
+ runtime.on("history", onHistory);
717
+ return () => {
718
+ runtime.off("configuration", onConfiguration);
719
+ runtime.off("history", onHistory);
720
+ };
721
+ }, [router, runtime]);
722
+ return shellConfiguration;
723
+ }
724
+
725
+ //#endregion
726
+ //#region source/web/react/extension/experience-shell-app.tsx
727
+ /** The fallback UI shown while waiting for the Experience Cloud shell to become ready. */
728
+ function ShellFallback() {
729
+ return /* @__PURE__ */ jsx(CenteredProgress, { "aria-label": "Loading experience cloud runtime" });
730
+ }
731
+ /**
732
+ * Renders the Experience Cloud shell flow, waiting for the shell's runtime configuration.
733
+ * @param props - The props needed to initialize the extension app.
734
+ */
735
+ function ExperienceShellExtensionApp(props) {
736
+ const { initialConfigurationPromise, runtime } = props;
737
+ return /* @__PURE__ */ jsx(Provider, {
738
+ colorScheme: void 0,
739
+ children: /* @__PURE__ */ jsx(Suspense, {
740
+ fallback: /* @__PURE__ */ jsx(ShellFallback, {}),
741
+ children: /* @__PURE__ */ jsx(ShellExtensionContent, {
742
+ initialConfigurationPromise,
743
+ runtime
744
+ })
745
+ })
746
+ });
747
+ }
748
+ /**
749
+ * Renders IMS-gated route content once the shell/runtime configuration is known.
750
+ * @param props - The props needed to initialize the extension app.
751
+ */
752
+ function ShellExtensionContent(props) {
753
+ const { initialConfigurationPromise, runtime } = props;
754
+ const shellConfiguration = useShellConfiguration(runtime, use(initialConfigurationPromise));
755
+ const spectrumRouter = useSpectrumRouter();
756
+ return /* @__PURE__ */ jsx(Provider, {
757
+ colorScheme: useExtensionColorScheme(shellConfiguration),
758
+ router: spectrumRouter,
759
+ children: /* @__PURE__ */ jsx(ExtensionErrorBoundary, { children: /* @__PURE__ */ jsx(ImsContextProvider, {
760
+ credentials: resolveShellImsCredentials(shellConfiguration),
761
+ children: /* @__PURE__ */ jsx(Outlet, {})
762
+ }) })
763
+ });
764
+ }
765
+
766
+ //#endregion
767
+ //#region source/web/react/extension/standalone-app.tsx
768
+ /** Renders the raw HTML page running standalone, with no host frame at all. Nothing to wait on. */
769
+ function StandaloneExtensionApp() {
770
+ const spectrumRouter = useSpectrumRouter();
771
+ useEffect(() => {
772
+ syncRootColorScheme(void 0);
773
+ }, []);
774
+ return /* @__PURE__ */ jsx(Provider, {
775
+ colorScheme: void 0,
776
+ router: spectrumRouter,
777
+ children: /* @__PURE__ */ jsx(ExtensionErrorBoundary, { children: /* @__PURE__ */ jsx(ImsContextProvider, {
778
+ credentials: null,
779
+ children: /* @__PURE__ */ jsx(Outlet, {})
780
+ }) })
781
+ });
782
+ }
783
+
784
+ //#endregion
785
+ //#region source/web/react/extension/entrypoint.tsx
786
+ /**
787
+ * The Entrypoint component is the main entry point for an extension app.
788
+ * It picks between the Commerce Admin, Experience Cloud shell, and standalone flows.
789
+ *
790
+ * @param props - The props needed to initialize the extension app.
791
+ */
792
+ function Entrypoint(props) {
793
+ const { extensionId, initialConfigurationPromise, runtime } = props;
794
+ if (isUiFrame() || isControlFrame()) return /* @__PURE__ */ jsx(CommerceExtensionApp, { extensionId });
795
+ if (isEmbeddedInHost() && initialConfigurationPromise !== null) return /* @__PURE__ */ jsx(ExperienceShellExtensionApp, {
796
+ initialConfigurationPromise,
797
+ runtime
798
+ });
799
+ return /* @__PURE__ */ jsx(StandaloneExtensionApp, {});
800
+ }
801
+
802
+ //#endregion
803
+ //#region source/web/react/extension/create-app.tsx
804
+ /**
805
+ * Mounts a Commerce Admin UI iframe app and handles Experience Cloud Shell, UIX
806
+ * registration, shared-context attachment, routing, and Spectrum setup.
807
+ *
808
+ * The app is wrapped in React's `<StrictMode>`, so in development builds (e.g. when
809
+ * served via `aio app dev` or `aio app run`) components render twice and effects run
810
+ * an extra setup + cleanup cycle on mount. Production builds are unaffected.
811
+ *
812
+ * @param options - App bootstrap options.
813
+ *
814
+ * @example
815
+ * ```tsx
816
+ * import { createExtensionApp } from "@adobe/aio-commerce-lib-admin-ui/web";
817
+ * import { MainPage } from "./pages/main-page.jsx";
818
+ *
819
+ * createExtensionApp({
820
+ * metadata: { extensionId: "my-extension-id" },
821
+ * menu: <MainPage />,
822
+ * });
823
+ * ```
824
+ */
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.");
827
+ const rootElement = customRoot ?? document.getElementById("root");
828
+ if (!rootElement) throw new Error("Could not find an element with id \"root\".");
829
+ const root = createRoot(rootElement);
830
+ const render = (runtime, initialConfigurationPromise) => {
831
+ const router = createExtensionRouter(/* @__PURE__ */ jsx(Entrypoint, {
832
+ extensionId: metadata.extensionId,
833
+ initialConfigurationPromise,
834
+ runtime
835
+ }), menu === void 0 ? routes : [{
836
+ element: menu,
837
+ path: "/"
838
+ }, ...routes]);
839
+ root.render(/* @__PURE__ */ jsx(StrictMode, { children: /* @__PURE__ */ jsx(RouterProvider, { router }) }));
840
+ };
841
+ try {
842
+ loadExperienceCloudRuntime();
843
+ init(() => {
844
+ const runtime = Runtime();
845
+ const { promise, resolve } = Promise.withResolvers();
846
+ page.title = document.title;
847
+ runtime.on("ready", (configuration) => {
848
+ resolve(configuration ?? runtime.lastConfigurationPayload);
849
+ page.done().catch(() => {
850
+ console.warn("Failed to mark page as done in Experience Cloud Shell.");
851
+ });
852
+ });
853
+ render(runtime, promise);
854
+ });
855
+ } catch {
856
+ render(createMockRuntime(), null);
857
+ }
858
+ }
859
+
860
+ //#endregion
861
+ export { createExtensionApp, useCommerce, useHostConnection, useIms, useMassActionContext, useOrderViewButtonContext, useSharedContext };