/** * Operating mode for the adapter (the mode state machine). * * - `"production"` (the default): real verify + claim through the Fanfare * Shopify app proxy. Requires `shopify.proxyBaseUrl`; a missing proxy throws * `FanfareConfigurationError` at provider mount (fail-loud — never silently * degrade to the client-only no-op verifier). * - `"demo"`: for local development and Storybook ONLY — never a live * storefront. The checkout verifier is a client-only no-op: it enforces only * client-side grant expiry and does NO server-side checkout enforcement, so a * gated product is not actually protected. A gated product renders a visible * "demo — not production-safe" state in this mode, never a silent pass. */ export type FanfareMode = "production" | "demo"; /** * Facts about the integrator's Shopify store the adapter configures itself from. * `proxyBaseUrl` is the single switch for the whole adapter: REAL verify + claim * AND experience resolution all go through the Fanfare app proxy. When present the * adapter drives the proxy; its absence is only valid in `"demo"`. * * In `mode:"production"` (the default), the default experience source is * `{ source: "metafield" }`, whose live tier reads the product's experience ids * through the app proxy (`GET {proxyBaseUrl}/experiences/ids-by-handle/{key}`) * — the proxy resolves the `$app:` namespace server-side, so NO Storefront creds * or numeric app id are required. `proxyBaseUrl` is the only field and is * mode-gated: required in production, optional in demo. */ export interface ShopifyAdapterConfig { /** * App-proxy base, e.g. "https://shop.example.com/apps/fanfare/api". The * single switch for the adapter: real verify + claim AND experience resolution * all route through it. Required in `mode:"production"`; absent only in * `mode:"demo"`. This is the Fanfare Shopify app's app-proxy URL on your store. * * Use the domain the Shopify Online Store serves from — its primary domain * under Settings → Domains, which is `*.myshopify.com` until a custom domain * is made primary. It is the host that answers the app-proxy path without * redirecting, and is not necessarily the host of a headless page. Once a * custom primary domain exists, the myshopify host answers with a redirect, * and a browser cannot follow a redirect on a cross-origin request, so every * proxy call fails before a response is received. The page's origin must * also be permitted by the app's Allowed Domains setting. Such a failure logs * one console warning naming the configured host and both conditions. */ proxyBaseUrl?: string; } /** * A Shopify product as the Storefront API hands it to the storefront. This is the * ONLY product shape the Shopify adapter path deals in — a Fanfare * product/experience id never appears here. The adapter's job is to map this to a * Fanfare experience id. * * Experience resolution keys on `handle`, but tolerantly: the value is run * through `resolveProductLookupKey`, which accepts a bare handle, a storefront * product URL (reduced to its `/products/` slug), or a product * global/numeric id (forwarded for the app proxy to classify server-side). So a * `handle` is the normal input, but a caller that has only a product id still * resolves. At least one of `handle` or `id` is required; when both are present, * a non-blank `handle` takes precedence. */ export type ShopifyProductRef = { /** The product handle, a storefront product URL/path, or a product id. */ handle: string; /** The product GID, used when `handle` is blank. */ id?: string; } | { /** Optional when a product id is available. */ handle?: string; /** The product GID, e.g. "gid://shopify/Product/123". */ id: string; }; /** * Where the Shopify→experience mapping comes from. The provider's `experiences` * config; resolved by `useFanfareExperience(shopifyProduct)`. * * WHERE EXPERIENCE IDS COME FROM: a Fanfare experience id (e.g. `"exp_1"`) is * minted in the Fanfare dashboard/admin when you create the experience. The * Fanfare Shopify app writes the ids for a product into that product's * `experienceIds` metafield. So for `"static"` / `"map"` you supply the id * string yourself (copy it from the dashboard); for `"metafield"` the id is read * back out of the metafield the app already wrote — you supply nothing. * * - `{ source: "static", experienceId }` — one experience for the whole store. * `experienceId` is a BARE Fanfare id (`"exp_1"`). Resolves synchronously to * that id for every product. * - `{ source: "map", values }` — an SSR-preloaded `lookup key → experienceId` map. * Each VALUE is a BARE Fanfare id (`"exp_1"`), NOT a raw metafield string — * do not feed it the `["exp_1"]` JSON-array form (contrast `metafieldValues` * below, whose values are the raw metafield string). Resolves synchronously by * normalized lookup key; an uncovered product resolves `ungated`. * - `{ source: "metafield", ... }` (the default) — reads the product's * `experienceIds` metafield (the one the Fanfare app wrote). Resolution order: * a `resolveExperience` override if supplied; else a preloaded * `metafieldValues[key]` (SSR fast path, no network); else a live app-proxy * read (`GET {proxyBaseUrl}/experiences/ids-by-handle/{key}`) that resolves * the `$app:` namespace server-side. Resolution never throws: an uncovered * product resolves `ungated`; a failed live read resolves `error` with a typed * reason (see `resolveShopifyExperience`). * * Note the singular/plural asymmetry: `static`/`map` carry a single * `experienceId`, while the underlying metafield is `experienceIds` (plural, a * JSON array). Resolution always honors the FIRST id only — the platform is * single-experience-per-product today (see `parseFirstExperienceId`). * * The per-product knobs (`metafieldValues`, `resolveExperience`) live ONLY on the * `"metafield"` variant below — there is no top-level override; reach for them by * choosing `source: "metafield"`. */ export type ExperienceSource = { source: "static"; experienceId: string; } | { source: "map"; values: Record; } | { source: "metafield"; /** * SSR fast path: a preloaded `lookup key → raw experienceIds metafield value` * map. Each VALUE is the RAW metafield string — a JSON array of ids like * `'["exp_1"]'` (NOT a bare `"exp_1"`; contrast `map.values` above) — exactly * as the Fanfare app wrote it / as you SSR-loaded it. Resolution parses it * with `parseFirstExperienceId` (the first id wins) and performs NO network * call when the normalized key is present; when absent, it falls through to * the live app-proxy read. */ metafieldValues?: Record; /** * The escape hatch: your own product → experienceId mapping function. Use it * when neither a static id, a handle map, nor the metafield read fits — e.g. * you resolve from your own catalog/CMS. Return a BARE Fanfare id (`"exp_1"`) * or `null` for an uncovered product. When supplied it takes precedence over * both the preloaded map and the live metafield read. May be sync or async. * * It lives under `source: "metafield"` (rather than a separate top-level * source) because it overrides the metafield resolution chain and composes * with `metafieldValues` as the fallback path. */ resolveExperience?: (product: ShopifyProductRef) => string | null | Promise; }; /** The mode + store config the guard inspects. The provider calls this at mount * before constructing the internal verifier/claimer. */ export interface ShopifyConfigGuardInput { mode: FanfareMode; shopify: ShopifyAdapterConfig; } /** * Pure fail-loud guard for the mode state machine. Throws * `FanfareConfigurationError` when `mode:"production"` lacks * `shopify.proxyBaseUrl` — the one invariant that must hold before the provider * builds the real verifier/claimer. Internal: `FanfareShopifyProvider` calls it * at mount; it is not part of the public surface itself. */ export declare function assertShopifyConfig(config: ShopifyConfigGuardInput): void;