{"version":3,"sources":["../src/react.ts"],"sourcesContent":["/**\n * @cross-deck/web/react — React hooks for the Crossdeck SDK.\n *\n * Why this exists: `Crossdeck.isEntitled(\"pro\")` is a synchronous cache\n * read, but the cache populates asynchronously after `getEntitlements()`\n * lands. React has no way to know the cache changed, so a component\n * that calls `isEntitled` directly in a render path would show the\n * empty-cache result forever (until something else triggered a re-render).\n *\n * The `useEntitlement` hook below ties cache state to React state via\n * `onEntitlementsChange`, so the component re-renders the moment the\n * answer changes. After the first render, every subsequent check is a\n * sync cache hit — exactly the \"microsecond entitlement check\" the\n * SDK promises.\n *\n * Side effect: importing this module pulls in `react` as a peer\n * dependency. Consumers who don't use React shouldn't import it.\n *\n * SSR safety: `useEffect` is a no-op during server-side rendering, and\n * the initial state is conservative (`false` until proven otherwise),\n * so server output never claims a non-existent entitlement. The hook\n * hydrates correctly on the client.\n *\n * NorthStar §11.4 (reactive bindings): every SDK ships first-class\n * framework bindings so the canonical snippet stays one line. Web =>\n * React hook here. iOS => `@Observable` SwiftUI wrapper (when iOS SDK\n * ships). Android => Compose `State<Boolean>` wrapper (when Android\n * SDK ships).\n */\n\nimport { createElement, useEffect, useRef, useState, type CSSProperties, type ReactNode, type RefObject } from \"react\";\nimport { Crossdeck } from \"./singleton\";\nimport type { CrossdeckOptions } from \"./types\";\nimport type { TrustToken, TrustTokenStatus } from \"./trust\";\nexport type { TrustTokenStatus };\n\n// ─────────────────────────────────────────────────────────────────\n// <CrossdeckProvider> — one-line React integration.\n//\n// What it does for the consumer:\n//   1. Calls Crossdeck.init({ appId, publicKey, environment, ...rest })\n//      once on first mount. React StrictMode re-mounts are de-duped\n//      via a module-level \"initialised\" flag so init never runs twice.\n//   2. Mirrors the `userId` prop into the SDK identity:\n//        userId provided + changed → Crossdeck.identify(userId)\n//        userId removed (logout)   → Crossdeck.reset()\n//      Both calls are idempotent against the SDK — no extra wire-up.\n//   3. Renders children unchanged. No context, no provider DOM node —\n//      the SDK is a singleton, the \"provider\" is a side-effect mount\n//      point that happens to look like a React provider.\n//\n// Why this exists: the dashboard's Next.js install prompt promised a\n// <CrossdeckProvider userId={…} appId={…} publicKey={…} environment={…}>\n// for months while @cross-deck/web/react only shipped useEntitlement /\n// useEntitlements. Customers pasted the prompt verbatim, got an import\n// error, fell off onboarding. This component closes the gap so the\n// prompt's 8-line recipe is now accurate end-to-end.\n//\n// SSR safety: every side effect lives inside useEffect, which is a\n// no-op during server render. Server output is exactly `children`.\n// ─────────────────────────────────────────────────────────────────\n\nlet _moduleInitDone = false;\n\ninterface CrossdeckProviderProps extends Omit<CrossdeckOptions, \"userId\"> {\n  /**\n   * Optional. When defined, the provider calls Crossdeck.identify(userId)\n   * after init and on every change. When the prop flips back to undefined\n   * (logout), the provider calls Crossdeck.reset().\n   *\n   * Pass your auth library's stable user id directly:\n   *   <CrossdeckProvider userId={session?.user?.id} … />          // NextAuth\n   *   <CrossdeckProvider userId={user?.uid} … />                  // Firebase\n   *   <CrossdeckProvider userId={supabase.auth.user()?.id} … />   // Supabase\n   *\n   * Anonymous (pre-login) traffic stays anonymous until userId becomes\n   * defined — the SDK's anonymousId follows the same user record once\n   * identify lands, so attribution survives sign-up.\n   */\n  userId?: string | null | undefined;\n  children: ReactNode;\n}\n\nexport function CrossdeckProvider(props: CrossdeckProviderProps): ReactNode {\n  const { userId, children, ...initOptions } = props;\n  // Track the last userId we sent to identify(), so we don't re-call\n  // identify on every render — only on actual transitions. Module-scope\n  // `_moduleInitDone` covers init de-dup; this ref covers identify.\n  const lastUserIdRef = useRef<string | null | undefined>(undefined);\n\n  // Init — once per module load, guarded against StrictMode's\n  // double-mount-in-dev so we never re-install the unload-flush\n  // listeners or reset the device-info cache.\n  useEffect(() => {\n    if (_moduleInitDone) return;\n    try {\n      Crossdeck.init(initOptions);\n      _moduleInitDone = true;\n    } catch (err) {\n      // Surface configuration errors loudly. The most common cause is\n      // a key/environment mismatch — letting it crash the provider\n      // tree would crash the whole app, which is worse than a console\n      // error during dogfood. Init failures are not recoverable\n      // mid-render, so we just log and let the rest of the app run\n      // un-initialized (every Crossdeck.* call will throw not_initialized\n      // until the operator fixes the config).\n      if (typeof console !== \"undefined\") {\n        console.error(\"[CrossdeckProvider] init failed:\", err);\n      }\n    }\n    // Intentionally no deps: init runs once. Changing appId / publicKey\n    // / environment at runtime is not supported (re-mount the provider\n    // on a new key — usually unnecessary in real apps).\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // Identity — mirror userId prop into SDK identity. Runs after init\n  // and on every userId change. No-op when the value matches what we\n  // last sent (avoids redundant network calls on parent re-renders).\n  useEffect(() => {\n    if (!_moduleInitDone) return;\n    if (lastUserIdRef.current === userId) return;\n    lastUserIdRef.current = userId;\n    try {\n      if (userId) {\n        // identify returns a Promise but we deliberately don't await\n        // it inside the effect — the cache populates async, and\n        // useEntitlement subscribes to mutations, so the UI catches up\n        // automatically the moment the response lands. Fire-and-forget\n        // is the documented pattern.\n        void Crossdeck.identify(userId);\n      } else {\n        Crossdeck.reset();\n      }\n    } catch (err) {\n      if (typeof console !== \"undefined\") {\n        console.error(\"[CrossdeckProvider] identity sync failed:\", err);\n      }\n    }\n  }, [userId]);\n\n  return children;\n}\n\n/**\n * Subscribe a React component to a single entitlement key.\n *\n * The hook returns the current `isEntitled(key)` value AND keeps it in\n * sync with the cache. When `getEntitlements()` lands, when a purchase\n * adds an entitlement, or when `reset()` is called on logout, every\n * component using this hook re-renders to reflect the change.\n *\n * Usage:\n *\n *   import { useEntitlement } from \"@cross-deck/web/react\";\n *\n *   function ProBadge() {\n *     const isPro = useEntitlement(\"pro\");\n *     return isPro ? <span className=\"badge\">Pro</span> : null;\n *   }\n *\n * Note that the hook does NOT call `getEntitlements()` itself — that's\n * a one-time boot warm-up the consumer is expected to trigger after\n * `Crossdeck.init()` (typically inside a top-level effect in their\n * Providers wrapper). Once warmed, every component using this hook\n * gets the answer for free.\n *\n * Pre-init: returns `false`. Calling Crossdeck.init() later doesn't\n * automatically refresh existing hook instances — but as soon as\n * something mutates the cache (i.e. after a successful\n * getEntitlements() call on the new SDK instance), the hook fires.\n */\nexport function useEntitlement(key: string): boolean {\n  // Initial value: read the cache synchronously if init() has happened.\n  // If not, default to false (the hook's contract: \"false until proven\n  // otherwise\"). Wrapping in a try/catch because isEntitled throws if\n  // called pre-init — we treat pre-init as \"not entitled yet.\"\n  const [isEntitled, setIsEntitled] = useState<boolean>(() => safeIsEntitled(key));\n\n  useEffect(() => {\n    // Re-read on mount in case init() ran between the initial state\n    // calculation and the effect attaching (rare in React, but happens\n    // with concurrent rendering / suspense boundaries).\n    setIsEntitled(safeIsEntitled(key));\n\n    let unsubscribe: (() => void) | null = null;\n    try {\n      unsubscribe = Crossdeck.onEntitlementsChange(() => {\n        setIsEntitled(safeIsEntitled(key));\n      });\n    } catch {\n      // Pre-init — onEntitlementsChange throws if the SDK hasn't been\n      // started yet. The hook will have to wait for the consumer to\n      // call init() and then for something to trigger a re-render\n      // through normal React means (parent state change, etc).\n      //\n      // Most apps init() once in a top-level Provider before any\n      // useEntitlement consumer mounts, so this is rare in practice.\n    }\n\n    return () => {\n      if (unsubscribe) unsubscribe();\n    };\n  }, [key]);\n\n  return isEntitled;\n}\n\n/**\n * Subscribe to the full entitlement list. Returns an array of active\n * entitlement keys, kept in sync with the cache. Useful for iterating\n * (e.g. rendering a list of unlocked features in a settings page).\n *\n * Same pre-init / SSR semantics as `useEntitlement`.\n */\nexport function useEntitlements(): readonly string[] {\n  const [keys, setKeys] = useState<readonly string[]>(() => safeListKeys());\n\n  useEffect(() => {\n    setKeys(safeListKeys());\n\n    let unsubscribe: (() => void) | null = null;\n    try {\n      unsubscribe = Crossdeck.onEntitlementsChange((entitlements) => {\n        setKeys(entitlements.filter((e) => e.isActive).map((e) => e.key));\n      });\n    } catch {\n      // Pre-init — see useEntitlement for rationale.\n    }\n\n    return () => {\n      if (unsubscribe) unsubscribe();\n    };\n  }, []);\n\n  return keys;\n}\n\nfunction safeIsEntitled(key: string): boolean {\n  try {\n    return Crossdeck.isEntitled(key);\n  } catch {\n    return false;\n  }\n}\n\nfunction safeListKeys(): readonly string[] {\n  try {\n    return Crossdeck.listEntitlements()\n      .filter((e) => e.isActive)\n      .map((e) => e.key);\n  } catch {\n    return [];\n  }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Crossdeck Trust — the human-proof panel as a first-class React surface.\n//\n// <CrossdeckTrust onToken={...} /> renders the branded, un-restylable Trust\n// iframe and hands you the minted token. `useTrustToken()` is the headless\n// equivalent for when you want the value in state. Both sit on\n// `Crossdeck.trust.panel(...)`, take the publishable key from the Provider's\n// init(), and are fail-open: if the panel can't mint, your signup still works\n// and the server scores the absent token. Never throws, never blocks the form.\n// ─────────────────────────────────────────────────────────────────\n\nexport interface CrossdeckTrustProps {\n  /**\n   * The project's publishable key (cd_pub_…). Pass it here — the robust, explicit way\n   * (like Stripe / Turnstile) — and no `Crossdeck.init()` / `<CrossdeckProvider>` is\n   * needed. Omit it and the SDK falls back to the key from `init()`.\n   */\n  publicKey?: string;\n  /** Called once when the panel mints a token. Pass `t.token` to your gate call. */\n  onToken?: (t: TrustToken) => void;\n  /**\n   * Called if the panel could not mint (adblocker, offline, our outage, timeout).\n   * INFORMATIONAL — not an error to handle. The signup should still proceed.\n   */\n  onUnavailable?: (reason: string) => void;\n  /** Class on the wrapper element the panel mounts into. */\n  className?: string;\n  /** Inline style on the wrapper element. */\n  style?: CSSProperties;\n  /** id on the wrapper element. */\n  id?: string;\n}\n\n/**\n * `<CrossdeckTrust onToken={setToken} />` — drop it on your signup form. Renders\n * the same cross-origin Trust panel every install gets, mints a single-use\n * attestation, and calls `onToken` with it. SSR-safe (mounts on the client).\n */\nexport function CrossdeckTrust(props: CrossdeckTrustProps): ReactNode {\n  const { publicKey, onToken, onUnavailable, className, style, id } = props;\n  const hostRef = useRef<HTMLDivElement | null>(null);\n  // Keep the latest callbacks without re-mounting the panel on every render.\n  const onTokenRef = useRef(onToken);\n  onTokenRef.current = onToken;\n  const onUnavailRef = useRef(onUnavailable);\n  onUnavailRef.current = onUnavailable;\n\n  useEffect(() => {\n    if (!hostRef.current) return;\n    const handle = Crossdeck.trust.panel({\n      target: hostRef.current,\n      publicKey,\n      onToken: (t) => onTokenRef.current?.(t),\n      onUnavailable: (r) => onUnavailRef.current?.(r),\n    });\n    return () => handle.destroy();\n    // Mount once — the panel mints once per lifecycle. Re-mount to re-mint.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return createElement(\"div\", { ref: hostRef, className, style, id });\n}\n\n/**\n * Headless Trust — mount the panel and read the token from React state.\n *\n * @example\n * const { ref, token, status } = useTrustToken();\n * return <><input name=\"email\" /><div ref={ref} /></>;\n * // then send `token` to your server; `status` is \"pending\" | \"ready\" | \"unavailable\".\n */\nexport function useTrustToken(opts?: {\n  /** Explicit publishable key (cd_pub_…) — no `init()` needed. Falls back to init's key. */\n  publicKey?: string;\n}): {\n  /** Attach to the element the panel should mount into: `<div ref={ref} />`. */\n  ref: RefObject<HTMLDivElement | null>;\n  /** The minted token, or null until it mints (or if the panel failed open). */\n  token: string | null;\n  /** Lifecycle: pending → ready (minted) or unavailable (failed open). */\n  status: TrustTokenStatus;\n} {\n  const publicKey = opts?.publicKey;\n  const ref = useRef<HTMLDivElement | null>(null);\n  const [token, setToken] = useState<string | null>(null);\n  const [status, setStatus] = useState<TrustTokenStatus>(\"pending\");\n\n  useEffect(() => {\n    if (!ref.current) return;\n    const handle = Crossdeck.trust.panel({\n      target: ref.current,\n      publicKey,\n      onToken: (t) => {\n        setToken(t.token);\n        setStatus(\"ready\");\n      },\n      onUnavailable: () => setStatus(\"unavailable\"),\n    });\n    return () => handle.destroy();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return { ref, token, status };\n}\n"],"mappings":"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","names":[]}