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