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