import { useEffect, useState } from "react" import { CustomBlockInitError } from "../bridge/messages/init.js" import { type CustomBlockInitial, type InitCustomBlockOptions, initCustomBlock, NotInIframeError, } from "../init.js" export type CustomBlockInitFailure = CustomBlockInitError | NotInIframeError /** * Discriminated state returned by {@link useCustomBlockInit}. * * Branch on `isLoaded`/`error`: * - `{ isLoaded: false, error: undefined }` — handshake in progress. * - `{ isLoaded: false, error: CustomBlockInitFailure }` — handshake failed (most commonly a * `CustomBlockInitError` with code `init_timeout` because the host never sent `init`). * - `{ isLoaded: true, initial }` — handshake complete; safe to render * children that call `useTheme`, `useBlockId`, etc. */ export type UseCustomBlockInitResult = | { isLoaded: false; error: undefined } | { isLoaded: false; error: CustomBlockInitFailure } | { isLoaded: true; error: undefined; initial: CustomBlockInitial } /** * React wrapper around {@link initCustomBlock}. Kicks off the SDK ↔ host * handshake on mount and returns a discriminated state object so the rest of * the tree can render inside the `isLoaded === true` branch (where every * other SDK hook is guaranteed to return a populated value). * * Idempotent — multiple components can call this; they share the same * underlying handshake promise. * * @example * function Root() { * const init = useCustomBlockInit() * if (init.error) return

Init failed: {init.error.message}

* if (!init.isLoaded) return null * return * } */ export function useCustomBlockInit( opts?: InitCustomBlockOptions, ): UseCustomBlockInitResult { const [state, setState] = useState({ isLoaded: false, error: undefined, }) useEffect(() => { let cancelled = false initCustomBlock(opts).then( initial => { if (!cancelled) { setState({ isLoaded: true, error: undefined, initial }) } }, err => { if (!cancelled) { setState({ isLoaded: false, error: normalizeInitError(err), }) } }, ) return () => { cancelled = true } // `initCustomBlock` caches its result, so options after the first call // are ignored — re-running on opts changes would be misleading. // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return state } function normalizeInitError(error: unknown): CustomBlockInitFailure { if ( error instanceof CustomBlockInitError || error instanceof NotInIframeError ) { return error } const message = error instanceof Error ? error.message : String(error) return new CustomBlockInitError({ code: "unknown_error", message, isRetryable: false, }) }