{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "paddle-client",
  "type": "registry:lib",
  "title": "Paddle client utilities",
  "description": "Paddle.js initialization singleton, SDK type re-exports, checkout event mapper, and SDK-layer hooks used by all client-side Paddle blocks.",
  "dependencies": ["@paddle/paddle-js"],
  "registryDependencies": ["@paddle/paddle-helpers"],
  "files": [
    {
      "path": "registry/new-york/blocks/paddle-client/lib/paddle-instance.ts",
      "content": "import {\n  initializePaddle,\n  type Environments,\n  type Paddle,\n  type PaddleEventData,\n} from \"@paddle/paddle-js\"\n\ntype PaddleEventListener = (event: PaddleEventData) => void\n\n// Module-level singleton state\nlet paddlePromise: Promise<Paddle | undefined> | null = null\nlet currentToken: string | null = null\nconst eventListeners = new Set<PaddleEventListener>()\n\n/**\n * Returns a shared Paddle instance, initializing once per token.\n * Uses a dispatcher eventCallback so multiple hooks can subscribe to events.\n */\nexport function getOrCreatePaddle(\n  token: string,\n  environment: Environments\n): Promise<Paddle | undefined> {\n  if (paddlePromise && currentToken === token) {\n    return paddlePromise\n  }\n\n  currentToken = token\n  paddlePromise = initializePaddle({\n    token,\n    environment,\n    eventCallback: (event) => {\n      eventListeners.forEach((listener) => listener(event))\n    },\n  })\n\n  return paddlePromise\n}\n\n/** Subscribe to Paddle events. Returns an unsubscribe function. */\nexport function addPaddleEventListener(listener: PaddleEventListener): () => void {\n  eventListeners.add(listener)\n  return () => {\n    eventListeners.delete(listener)\n  }\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/new-york/blocks/paddle-client/lib/paddle-sdk-types.ts",
      "content": "// Types re-exported from @paddle/paddle-js for convenience.\n// Import directly from @paddle/paddle-js if you prefer.\nexport type {\n  Paddle,\n  Environments,\n  PaddleEventData,\n  PricePreviewParams,\n  PricePreviewResponse,\n  CheckoutEventsData,\n  CheckoutCustomer,\n  TimePeriod,\n  CheckoutOpenLineItem,\n} from \"@paddle/paddle-js\"\n\nexport { CheckoutEventNames } from \"@paddle/paddle-js\"\n\n// CheckoutSettings from the SDK, widened to include \"express\" in the variant\n// union. The express variant is a valid Paddle.js variant not yet reflected in\n// the SDK's Variant type ('multi-page' | 'one-page').\nimport type { CheckoutSettings as SDKCheckoutSettings, CheckoutCustomer } from \"@paddle/paddle-js\"\nexport type CheckoutSettings = Omit<SDKCheckoutSettings, \"variant\"> & {\n  variant?: SDKCheckoutSettings[\"variant\"] | \"express\"\n}\n\n// ---\n// Hook API types\n// Used by the hooks in this library. You generally won't need to use these\n// directly unless you're building a custom integration on top of the hooks.\n// ---\n\n/**\n * Options for opening a Paddle inline checkout.\n *\n * Passed to `openCheckout()` returned by `useCheckout`. Accepts either a\n * `priceId` (legacy, mapped to a single-item checkout) or an `items` array\n * for multi-item checkouts. When both are provided, `items` takes precedence.\n *\n * Note: this is a simplified wrapper over the raw Paddle SDK `CheckoutOpenOptions`.\n * For full control over the checkout, use `paddle.Checkout.open()` directly.\n */\nexport type OpenCheckoutOptions = {\n  priceId: string\n  items?: Array<{ priceId: string; quantity?: number }>\n  customer?: CheckoutCustomer\n  customerAuthToken?: string\n  discountCode?: string\n  discountId?: string\n  customData?: Record<string, unknown>\n  /** Must be an absolute URL (starting with `https://` or `http://`) */\n  successUrl?: string\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/new-york/blocks/paddle-client/lib/hooks/use-paddle-prices.tsx",
      "content": "\"use client\"\n\nimport {\n  type Environments,\n  type Paddle,\n  type PricePreviewParams,\n  type PricePreviewResponse,\n} from \"@paddle/paddle-js\"\nimport { useEffect, useState, useRef, useCallback } from \"react\"\nimport {\n  formatBillingCycle,\n  formatTrialPeriod,\n} from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-format\"\nimport { getOrCreatePaddle } from \"@/registry/new-york/blocks/paddle-client/lib/paddle-instance\"\nimport type { PriceData } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\n\nexport type UsePaddlePricesArgs = {\n  clientToken: string\n  environment?: Environments\n  priceIds: string[]\n  countryCode?: string\n  discountId?: string\n}\n\ntype PaddlePrices = Record<string, PriceData>\n\nfunction getPriceAmounts(prices: PricePreviewResponse): PaddlePrices {\n  return prices.data.details.lineItems.reduce((acc, item) => {\n    const hasDiscount = item.discounts.length > 0\n    acc[item.price.id] = {\n      total: item.formattedTotals.total,\n      originalTotal: hasDiscount ? item.formattedTotals.subtotal : undefined,\n      interval: formatBillingCycle(item.price.billingCycle),\n      trialPeriod: item.price.trialPeriod ? formatTrialPeriod(item.price.trialPeriod) : undefined,\n    }\n    return acc\n  }, {} as PaddlePrices)\n}\n\nexport function usePaddlePrices(args: UsePaddlePricesArgs): {\n  prices: PaddlePrices\n  loading: boolean\n  error: Error | null\n} {\n  const { clientToken, environment = \"production\", priceIds, countryCode, discountId } = args\n  const [paddle, setPaddle] = useState<Paddle | null>(null)\n  const [prices, setPrices] = useState<PaddlePrices>({})\n  const [loading, setLoading] = useState<boolean>(true)\n  const [error, setError] = useState<Error | null>(null)\n\n  // Stable keys — prevent unnecessary re-fetches\n  const priceIdsKey = priceIds.join(\",\")\n  const discountIdKey = discountId ?? \"\"\n\n  // Initialize Paddle once\n  useEffect(() => {\n    if (paddle || !clientToken) return\n\n    getOrCreatePaddle(clientToken, environment)\n      .then((paddleInstance) => {\n        if (paddleInstance) {\n          setPaddle(paddleInstance)\n        }\n      })\n      .catch((err) => {\n        setError(err instanceof Error ? err : new Error(\"Failed to initialize Paddle\"))\n        setLoading(false)\n      })\n  }, [clientToken, environment, paddle])\n\n  // Fetch prices when Paddle is ready or inputs change\n  const fetchedRef = useRef<string | null>(null)\n\n  const fetchPrices = useCallback(async () => {\n    if (!paddle) return\n\n    const fetchKey = `${priceIdsKey}:${countryCode ?? \"\"}:${discountIdKey}`\n    if (fetchedRef.current === fetchKey) return\n    fetchedRef.current = fetchKey\n\n    const priceIdList = priceIdsKey.split(\",\").filter(Boolean)\n    if (priceIdList.length === 0) return\n\n    const paddlePricePreviewRequest: Partial<PricePreviewParams> = {\n      items: priceIdList.map((priceId) => ({ priceId, quantity: 1 })),\n      ...(countryCode && { address: { countryCode } }),\n      ...(discountId && { discountId }),\n    }\n\n    setLoading(true)\n\n    try {\n      const priceResponse = await paddle.PricePreview(\n        paddlePricePreviewRequest as PricePreviewParams\n      )\n      setPrices(getPriceAmounts(priceResponse))\n      setError(null)\n    } catch (err) {\n      setError(err instanceof Error ? err : new Error(\"Failed to fetch prices\"))\n    } finally {\n      setLoading(false)\n    }\n  }, [paddle, priceIdsKey, countryCode, discountIdKey])\n\n  useEffect(() => {\n    fetchPrices()\n  }, [fetchPrices])\n\n  return { prices, loading, error }\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/new-york/blocks/paddle-client/lib/hooks/use-checkout.tsx",
      "content": "\"use client\"\n\nimport { type Environments, type Theme, type Paddle } from \"@paddle/paddle-js\"\nimport { useEffect, useState, useCallback, useRef } from \"react\"\nimport {\n  CheckoutEventNames,\n  type OpenCheckoutOptions,\n  type CheckoutSettings,\n} from \"@/registry/new-york/blocks/paddle-client/lib/paddle-sdk-types\"\nimport type { CheckoutCompleteData } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\nimport {\n  getOrCreatePaddle,\n  addPaddleEventListener,\n} from \"@/registry/new-york/blocks/paddle-client/lib/paddle-instance\"\n\nexport type UseCheckoutArgs = {\n  clientToken: string\n  environment?: Environments\n  theme?: Theme\n  locale?: string\n  showNonExpressPaymentMethods?: boolean\n  checkoutSettings: CheckoutSettings\n  onComplete?: (data: CheckoutCompleteData) => void\n  onError?: (error: Error) => void\n}\n\nexport function useCheckout(args: UseCheckoutArgs) {\n  const {\n    clientToken,\n    environment = \"production\",\n    theme,\n    locale,\n    showNonExpressPaymentMethods,\n    checkoutSettings,\n    onComplete,\n    onError,\n  } = args\n\n  const [paddle, setPaddle] = useState<Paddle | null>(null)\n  const [isReady, setIsReady] = useState(false)\n\n  // Refs avoid stale closures in event listeners\n  const onCompleteRef = useRef(onComplete)\n  const onErrorRef = useRef(onError)\n\n  useEffect(() => {\n    onCompleteRef.current = onComplete\n    onErrorRef.current = onError\n  }, [onComplete, onError])\n\n  // Subscribe to checkout.completed events\n  useEffect(() => {\n    const unsubscribe = addPaddleEventListener((event) => {\n      if (event.name === CheckoutEventNames.CHECKOUT_COMPLETED && onCompleteRef.current) {\n        onCompleteRef.current({\n          transactionId: event.data?.transaction_id ?? \"\",\n          customerId: event.data?.customer?.id ?? \"\",\n          customerEmail: event.data?.customer?.email ?? \"\",\n        })\n      }\n    })\n\n    return unsubscribe\n  }, [])\n\n  // Initialize Paddle\n  useEffect(() => {\n    if (paddle || !clientToken) return\n\n    getOrCreatePaddle(clientToken, environment)\n      .then((paddleInstance) => {\n        if (paddleInstance) {\n          setPaddle(paddleInstance)\n          setIsReady(true)\n        }\n      })\n      .catch((error) => {\n        if (onErrorRef.current) {\n          onErrorRef.current(\n            error instanceof Error ? error : new Error(\"Failed to initialize Paddle\")\n          )\n        }\n      })\n  }, [clientToken, environment, paddle])\n\n  const openCheckout = useCallback(\n    (options: OpenCheckoutOptions) => {\n      if (!paddle || !isReady) {\n        console.warn(\"Paddle not initialized yet\")\n        return\n      }\n\n      // Normalise to items array — support both items[] and legacy priceId\n      const items =\n        options.items && options.items.length > 0\n          ? options.items.map((item) => ({ priceId: item.priceId, quantity: item.quantity ?? 1 }))\n          : [{ priceId: options.priceId, quantity: 1 }]\n\n      paddle.Checkout.open({\n        items,\n        ...(options.customerAuthToken\n          ? { customerAuthToken: options.customerAuthToken }\n          : options.customer\n            ? { customer: options.customer }\n            : {}),\n        ...(options.discountCode\n          ? { discountCode: options.discountCode }\n          : options.discountId\n            ? { discountId: options.discountId }\n            : {}),\n        ...(options.customData && { customData: options.customData }),\n        ...(options.successUrl && { successUrl: options.successUrl }),\n        settings: {\n          displayMode: checkoutSettings.displayMode,\n          variant: checkoutSettings.variant,\n          frameTarget: checkoutSettings.frameTarget,\n          frameInitialHeight: checkoutSettings.frameInitialHeight,\n          frameStyle: checkoutSettings.frameStyle ?? \"width: 100%; border: 0;\",\n          ...(theme && { theme }),\n          ...(locale && { locale }),\n          // Not yet in @paddle/paddle-js SDK types, landing soon\n          ...(showNonExpressPaymentMethods !== undefined && {\n            showNonExpressPaymentMethods,\n          }),\n        } as Record<string, unknown>,\n      })\n    },\n    [paddle, isReady, checkoutSettings, theme, locale, showNonExpressPaymentMethods]\n  )\n\n  const closeCheckout = useCallback(() => {\n    if (paddle?.Checkout) {\n      paddle.Checkout.close()\n    }\n  }, [paddle])\n\n  const updateItems = useCallback(\n    (items: Array<{ priceId: string; quantity: number }>) => {\n      if (!paddle || !isReady) {\n        console.warn(\"Paddle not initialized yet\")\n        return\n      }\n      ;(\n        paddle.Checkout as Record<string, unknown> & { updateItems?: (items: unknown) => void }\n      ).updateItems?.(items)\n    },\n    [paddle, isReady]\n  )\n\n  return {\n    openCheckout,\n    closeCheckout,\n    updateItems,\n    isReady,\n  }\n}\n",
      "type": "registry:hook"
    }
  ],
  "envVars": {
    "NEXT_PUBLIC_PADDLE_CLIENT_TOKEN": "",
    "NEXT_PUBLIC_PADDLE_ENV": "sandbox"
  },
  "docs": "Create a client-side token at Paddle > Developer Tools > Authentication. Sandbox tokens start with test_ (use sandbox-vendors.paddle.com). Live tokens start with live_ (use vendors.paddle.com). Set NEXT_PUBLIC_PADDLE_ENV to 'sandbox' or 'production' to match your token. See: https://developer.paddle.com/paddlejs/client-side-tokens"
}
