(undefined);
+ const { data, error, isLoading } = useEnabledStoreUiExtensions({
+ target: id,
+ storeId,
+ enabled: !apps,
+ });
+
+ const providedExtensions = useMemo(
+ () =>
+ apps?.flatMap(app =>
+ app.uiExtensions.filter(extension => extension.target === id)
+ ),
+ [apps, id]
+ );
+
+ const targetExtensions = providedExtensions || data || [];
+ const context = useMemo(
+ () =>
+ buildUiExtensionContext({
+ id,
+ storeId,
+ orderId,
+ locale,
+ currencyCode,
+ theme,
+ }),
+ [id, storeId, orderId, locale, currencyCode, theme]
+ );
+
+ useEffect(() => {
+ if (!debug || !targetExtensions.length) return;
+
+ const initialPropKeys = getInitialPropKeys(initialProps);
+ const signature = getDebugLogSignature({
+ context,
+ id,
+ initialPropKeys,
+ targetExtensions,
+ });
+
+ if (lastDebugLogSignatureRef.current === signature) {
+ return;
+ }
+
+ lastDebugLogSignatureRef.current = signature;
+
+ // biome-ignore lint/suspicious/noConsole: debug mode intentionally exposes extension metadata to developers
+ console.info('[GoDaddy UI Extensions]', {
+ target: id,
+ uiExtensions: targetExtensions,
+ context,
+ initialPropKeys,
+ });
+ }, [debug, id, targetExtensions, context, initialProps]);
+
+ if (targetExtensions.length) {
+ return (
+ <>
+ {targetExtensions.map(extension => (
+
+ ))}
+ >
+ );
+ }
+
+ if (error) {
+ const message = error instanceof Error ? error.message : String(error);
+
+ return {JSON.stringify({ error: message }, null, 2)};
+ }
+
+ if (isLoading) {
+ return null;
+ }
+
+ return null;
+}
diff --git a/packages/react/src/ui-extensions/types.ts b/packages/react/src/ui-extensions/types.ts
new file mode 100644
index 00000000..a5e2147b
--- /dev/null
+++ b/packages/react/src/ui-extensions/types.ts
@@ -0,0 +1,73 @@
+import type { ResultOf } from 'gql.tada';
+import { GetEnabledStoreUiExtensionsQuery } from '@/lib/godaddy/checkout-queries';
+import type {
+ UiExtensionContext,
+ UiExtensionInitialProps,
+ UiExtensionRuntimeError,
+ UiExtensionRuntimeType,
+} from './runtime/types';
+
+export type UiExtensionTargetId = string;
+
+export interface UiExtension {
+ id: string;
+ applicationId?: string | null;
+ releaseId?: string | null;
+ name?: string | null;
+ handle?: string | null;
+ cdnUrl?: string | null;
+ type: string;
+ target?: string | null;
+}
+
+export interface UiExtensionAppRelease {
+ id: string;
+ version: string;
+ uiExtensions: UiExtension[];
+}
+
+export interface EnabledUiExtensionApp {
+ id: string;
+ name: string;
+ release?: UiExtensionAppRelease | null;
+}
+
+export interface EnabledStoreUiExtensionApp extends EnabledUiExtensionApp {
+ uiExtensions: UiExtension[];
+}
+
+export interface TargetProps {
+ id: UiExtensionTargetId;
+ storeId?: string;
+ orderId?: string;
+ apps?: EnabledStoreUiExtensionApp[];
+ runtime?: Extract;
+ initialProps?: UiExtensionInitialProps;
+ locale?: string;
+ currencyCode?: string;
+ theme?: UiExtensionContext['theme'];
+ onExtensionError?(error: UiExtensionRuntimeError): void;
+}
+
+export interface UseEnabledStoreUiExtensionsOptions {
+ target: UiExtensionTargetId;
+ storeId?: string;
+ enabled?: boolean;
+}
+
+export interface UseCheckoutUiExtensionAppsOptions {
+ targets: UiExtensionTargetId[];
+}
+
+export type EnabledStoreUiExtensionsData = ResultOf<
+ typeof GetEnabledStoreUiExtensionsQuery
+>;
+
+export function withReleaseUiExtensions(
+ app: EnabledUiExtensionApp
+): EnabledStoreUiExtensionApp {
+ return {
+ ...app,
+ uiExtensions: app.release?.uiExtensions || [],
+ };
+}
diff --git a/packages/react/src/ui-extensions/utils.ts b/packages/react/src/ui-extensions/utils.ts
new file mode 100644
index 00000000..7cb8fa3c
--- /dev/null
+++ b/packages/react/src/ui-extensions/utils.ts
@@ -0,0 +1,41 @@
+import type {
+ EnabledStoreUiExtensionApp,
+ EnabledUiExtensionApp,
+} from './types';
+import { withReleaseUiExtensions } from './types';
+
+export function groupAppsByUiExtensionTarget(
+ apps?: EnabledUiExtensionApp[] | null
+) {
+ const grouped: Record = {};
+
+ for (const enabledApp of apps || []) {
+ const app = withReleaseUiExtensions(enabledApp);
+ const uiExtensionsByTarget = new Map();
+
+ for (const extension of app.uiExtensions) {
+ if (!extension.target) continue;
+
+ uiExtensionsByTarget.set(extension.target, [
+ ...(uiExtensionsByTarget.get(extension.target) || []),
+ extension,
+ ]);
+ }
+
+ for (const [target, uiExtensions] of uiExtensionsByTarget) {
+ grouped[target] ??= [];
+ grouped[target].push({
+ ...app,
+ release: app.release
+ ? {
+ ...app.release,
+ uiExtensions,
+ }
+ : app.release,
+ uiExtensions,
+ });
+ }
+ }
+
+ return grouped;
+}
diff --git a/packages/react/tsdown.config.ts b/packages/react/tsdown.config.ts
index b9087090..a42406f9 100644
--- a/packages/react/tsdown.config.ts
+++ b/packages/react/tsdown.config.ts
@@ -5,7 +5,7 @@ import { defineConfig } from 'tsdown';
const execAsync = promisify(exec);
export default defineConfig({
- entry: ['src/index.ts', 'src/server.ts'],
+ entry: ['src/index.ts', 'src/server.ts', 'src/ui-extensions/index.ts'],
plugins: [],
tsconfig: './tsconfig.json',
dts: true,