{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "inline-checkout",
  "type": "registry:block",
  "title": "Inline checkout",
  "description": "Inline checkout block with order summary, Paddle payment frame, and real-time event updates. Supports reactive item changes, plan switching, and quantity updates.",
  "dependencies": [],
  "registryDependencies": [
    "@paddle/paddle-client",
    "@paddle/checkout-summary",
    "card"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/inline-checkout/components/inline-checkout.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CheckoutSummary } from \"@/registry/new-york/blocks/checkout-summary/components/checkout-summary\"\nimport { useCheckout } from \"@/registry/new-york/blocks/paddle-client/lib/hooks/use-checkout\"\nimport { addPaddleEventListener } from \"@/registry/new-york/blocks/paddle-client/lib/paddle-instance\"\nimport { mapCheckoutEventsToSummary } from \"@/registry/new-york/blocks/checkout-summary/lib/checkout-summary-utils\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  CheckoutEventNames,\n  type Environments,\n  type CheckoutCustomer,\n  type CheckoutEventsData,\n  type PaddleEventData,\n  type CheckoutOpenLineItem,\n} from \"@/registry/new-york/blocks/paddle-client/lib/paddle-sdk-types\"\nimport type {\n  CheckoutCompleteData,\n  CheckoutSummaryData,\n} from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\n\nconst INLINE_CHECKOUT_FRAME_TARGET = \"paddle-inline-checkout-frame\"\n\nconst SUMMARY_EVENT_NAMES = new Set([\n  CheckoutEventNames.CHECKOUT_LOADED,\n  CheckoutEventNames.CHECKOUT_UPDATED,\n  CheckoutEventNames.CHECKOUT_ITEMS_UPDATED,\n  CheckoutEventNames.CHECKOUT_CUSTOMER_CREATED,\n  CheckoutEventNames.CHECKOUT_CUSTOMER_UPDATED,\n  CheckoutEventNames.CHECKOUT_DISCOUNT_APPLIED,\n  CheckoutEventNames.CHECKOUT_DISCOUNT_REMOVED,\n])\n\n/** Props for the `InlineCheckout` component. */\nexport type InlineCheckoutProps = {\n  clientToken: string\n  environment?: Environments\n  items: CheckoutOpenLineItem[]\n\n  variant?: \"one-page\" | \"multi-page\"\n  theme?: \"light\" | \"dark\"\n  locale?: string\n  /** Must be an absolute URL (starting with `https://` or `http://`) */\n  successUrl?: string\n  /** Controls where the order summary appears relative to the checkout frame. Defaults to \"start\" (summary on the left). */\n  summaryPosition?: \"start\" | \"end\" | \"top\" | \"bottom\"\n\n  customer?: CheckoutCustomer\n  customerAuthToken?: string\n  discountCode?: string\n  discountId?: string\n  customData?: Record<string, unknown>\n\n  policyUrl?: string\n  policyLabel?: string\n\n  /** Called when the Paddle checkout completes successfully */\n  onComplete?: (data: CheckoutCompleteData) => void\n  /** Called for every Paddle.js event emitted during checkout */\n  onEvent?: (event: PaddleEventData) => void\n  /** Called when the Paddle checkout fails to initialize or encounters an error */\n  onError?: (error: Error) => void\n\n  className?: string\n}\n\nexport function InlineCheckout({\n  clientToken,\n  environment = \"production\",\n  items,\n  variant = \"one-page\",\n  theme,\n  locale,\n  successUrl,\n  summaryPosition = \"start\",\n  customer,\n  customerAuthToken,\n  discountCode,\n  discountId,\n  customData,\n  policyUrl,\n  policyLabel,\n  onComplete,\n  onEvent,\n  onError,\n  className,\n}: InlineCheckoutProps) {\n  const [summaryData, setSummaryData] = React.useState<CheckoutSummaryData | undefined>(undefined)\n  const [initError, setInitError] = React.useState<Error | null>(null)\n\n  // Ref avoids stale closure in event listener\n  const onEventRef = React.useRef(onEvent)\n  React.useEffect(() => {\n    onEventRef.current = onEvent\n  }, [onEvent])\n\n  const { openCheckout, updateItems, isReady } = useCheckout({\n    clientToken,\n    environment,\n    theme,\n    locale,\n    checkoutSettings: {\n      displayMode: \"inline\",\n      variant,\n      frameTarget: INLINE_CHECKOUT_FRAME_TARGET,\n      frameInitialHeight: 450,\n      frameStyle: \"width: 100%; min-width: 312px; background-color: transparent; border: none;\",\n    },\n    onComplete,\n    onError: (error) => {\n      setInitError(error)\n      onError?.(error)\n    },\n  })\n\n  // Stable ref — effects re-run only when actual values change\n  const stableCustomer = React.useMemo(\n    () => customer,\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [\n      customer?.id,\n      customer?.email,\n      customer?.address?.id,\n      customer?.address?.countryCode,\n      customer?.address?.postalCode,\n      customer?.address?.region,\n      customer?.address?.city,\n      customer?.address?.firstLine,\n      customer?.business?.id,\n      customer?.business?.name,\n      customer?.business?.taxIdentifier,\n    ]\n  )\n\n  const stableCustomData = React.useMemo(\n    () => customData,\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [JSON.stringify(customData)]\n  )\n\n  const itemsKey = React.useMemo(\n    () => items.map((i) => `${i.priceId}:${i.quantity ?? 1}`).join(\",\"),\n    [items]\n  )\n\n  const isOpenRef = React.useRef(false)\n  // Tracks the last itemsKey applied to the open checkout so we can skip the\n  // redundant updateItems call that would otherwise fire on the same render\n  // cycle as openCheckout when isReady first becomes true.\n  const prevItemsKeyRef = React.useRef<string | null>(null)\n\n  // Open checkout once Paddle is ready\n  React.useEffect(() => {\n    if (!isReady || items.length === 0) return\n\n    if (!isOpenRef.current) {\n      isOpenRef.current = true\n      openCheckout({\n        priceId: items[0].priceId,\n        items,\n        ...(stableCustomer && { customer: stableCustomer }),\n        ...(customerAuthToken && { customerAuthToken }),\n        ...(discountCode ? { discountCode } : discountId ? { discountId } : {}),\n        ...(stableCustomData && { customData: stableCustomData }),\n        ...(successUrl && { successUrl }),\n      })\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isReady])\n\n  // Update items reactively after mount\n  React.useEffect(() => {\n    if (!isReady || !isOpenRef.current) return\n\n    // First run after openCheckout — record the baseline key and skip the call.\n    // openCheckout already applied these items; calling updateItems immediately\n    // after would be a redundant SDK call.\n    if (prevItemsKeyRef.current === null) {\n      prevItemsKeyRef.current = itemsKey\n      return\n    }\n\n    if (prevItemsKeyRef.current === itemsKey) return\n    prevItemsKeyRef.current = itemsKey\n    updateItems(items.map((i) => ({ priceId: i.priceId, quantity: i.quantity ?? 1 })))\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [itemsKey, isReady])\n\n  // Subscribe to all Paddle events: update summary + forward to onEvent\n  React.useEffect(() => {\n    const unsubscribe = addPaddleEventListener((event: PaddleEventData) => {\n      if (onEventRef.current) {\n        onEventRef.current(event)\n      }\n\n      if (event.name && SUMMARY_EVENT_NAMES.has(event.name) && event.data) {\n        setSummaryData(mapCheckoutEventsToSummary(event.data as CheckoutEventsData))\n      }\n    })\n\n    return unsubscribe\n  }, [])\n\n  if (initError) {\n    return (\n      <div\n        className={cn(\n          \"rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive\",\n          className\n        )}\n      >\n        Failed to open checkout. Please refresh and try again.\n      </div>\n    )\n  }\n\n  const isHorizontal = summaryPosition === \"start\" || summaryPosition === \"end\"\n  const summaryFirst = summaryPosition === \"start\" || summaryPosition === \"top\"\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-6\",\n        isHorizontal ? \"flex-col lg:flex-row lg:items-start\" : \"flex-col\",\n        className\n      )}\n    >\n      {summaryFirst && (\n        <div className={cn(\"w-full\", isHorizontal && \"lg:w-72 lg:shrink-0\")}>\n          <CheckoutSummary summary={summaryData} policyUrl={policyUrl} policyLabel={policyLabel} />\n        </div>\n      )}\n\n      <div className=\"min-w-0 flex-1\">\n        <div className={cn(INLINE_CHECKOUT_FRAME_TARGET, \"w-full min-h-[450px]\")} />\n      </div>\n\n      {!summaryFirst && (\n        <div className={cn(\"w-full\", isHorizontal && \"lg:w-72 lg:shrink-0\")}>\n          <CheckoutSummary summary={summaryData} policyUrl={policyUrl} policyLabel={policyLabel} />\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Each item requires a price ID (starts with pri_). Create and/or find yours in your Paddle dashboard under Catalog > Prices. Use sandbox price IDs while NEXT_PUBLIC_PADDLE_ENV is 'sandbox'.",
  "categories": ["checkout"]
}
