Skip to main content

import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';

useCheckoutButton / useCreateCheckoutSession

Two complementary hooks for managing checkout flows. useCheckoutButton handles Lemon Squeezy checkout interactions (embedded and redirect modes), while useCreateCheckoutSession manages Stripe checkout session creation with multi-currency support.

Import

// Lemon Squeezy checkout
import { useCheckoutButton } from '@/hooks/use-checkout-button';
import type { CheckoutButtonParams, UseCheckoutButtonReturn } from '@/hooks/use-checkout-button';

// Stripe checkout
import { useCreateCheckoutSession, useCheckoutSessionCache } from '@/hooks/use-create-checkout';

API Reference

useCheckoutButton

function useCheckoutButton(params?: CheckoutButtonParams): UseCheckoutButtonReturn;

CheckoutButtonParams

PropertyTypeDefaultDescription
defaultEmailstring''Pre-filled email for the checkout form.
defaultPricenumberundefinedDefault price in cents.
variantIdnumberundefinedLemon Squeezy product variant ID.
metadataRecord<string, any>{}Custom metadata to attach to the checkout.
embeddedbooleanfalseUse embedded checkout overlay instead of redirect.
onPaymentSuccess(res?: any) => voidundefinedCallback for successful payment (embedded mode).
onClose() => voidundefinedCallback when embedded checkout is closed.
darkbooleanundefinedEnable dark mode for the checkout UI.

Return Value -- UseCheckoutButtonReturn

State Properties:

PropertyTypeDescription
customPricenumber | undefinedThe current custom price value.
showFormbooleanWhether to show the email form (true when no default email is provided).
isLoadingbooleanWhether the checkout creation is in progress.
errorError | nullError from the last checkout attempt.
isErrorbooleanWhether the last operation resulted in an error.
isSuccessbooleanWhether the last checkout was created successfully.
checkoutUrlstring | nullThe generated checkout URL (embedded mode only).
isEmbedReadybooleanWhether the embedded checkout overlay is ready to display.

Action Properties:

PropertyTypeDescription
setCustomPrice(price: number | undefined) => voidUpdate the custom price.
handleSubmit(e: React.FormEvent) => Promise<void>Form submission handler that creates a checkout.
handleClick() => Promise<void>Simple click handler that creates a checkout.
handlePriceChange(e: React.ChangeEvent<HTMLInputElement>) => voidInput handler for price fields (validates numeric input).
clearCheckout() => voidClears the embedded checkout state.
handleSubmitWithParams(params?: CheckoutButtonParams) => Promise<void>Creates a checkout with custom override parameters.

useCreateCheckoutSession

function useCreateCheckoutSession(): UseCreateCheckoutSessionReturn;

Return Value

PropertyTypeDescription
createCheckoutSession(plan: PricingConfig, user: User | null, billingInterval: PaymentInterval) => Promise<string>Creates a Stripe checkout session and redirects to the Stripe-hosted payment page. Returns the session ID.
isLoadingbooleanWhether the session creation is in progress.
errorstring | nullError message from the last attempt.
isErrorbooleanWhether the last operation failed.
isSuccessbooleanWhether the last session was created successfully.
reset() => voidResets the mutation state.
datastring | undefinedThe checkout session ID from the last successful creation.
isPausedbooleanWhether the mutation is paused (e.g., due to network issues).
failureCountnumberNumber of failed attempts.
failureReasonError | nullThe error from the last failure.
canRetrybooleanWhether a retry is possible (fewer than 2 failures).

useCheckoutSessionCache

function useCheckoutSessionCache(): {
invalidateCheckoutCache: () => Promise<void>;
clearCheckoutCache: () => Promise<void>;
};
PropertyTypeDescription
invalidateCheckoutCache() => Promise<void>Invalidates checkout-related query caches, triggering background refetches.
clearCheckoutCache() => Promise<void>Completely removes checkout session data from the query cache.

Usage Examples

Lemon Squeezy Redirect Checkout

function BuyButton({ variantId }: { variantId: number }) {
const { handleClick, isLoading, isError, error } = useCheckoutButton({
variantId,
metadata: { source: 'pricing-page' },
});

return (
<div>
<button onClick={handleClick} disabled={isLoading}>
{isLoading ? 'Processing...' : 'Buy Now'}
</button>
{isError && <p className="text-red-500">{error?.message}</p>}
</div>
);
}

Stripe Subscription Checkout

function SubscriptionButton({ plan, billingInterval }: {
plan: PricingConfig;
billingInterval: PaymentInterval;
}) {
const { createCheckoutSession, isLoading, error, canRetry } = useCreateCheckoutSession();
const { data: session } = useSession();

const handleSubscribe = async () => {
try {
await createCheckoutSession(plan, session?.user ?? null, billingInterval);
// Redirect happens automatically via window.location.href
} catch (err) {
console.error('Checkout failed:', err);
}
};

return (
<div>
<button onClick={handleSubscribe} disabled={isLoading}>
{isLoading ? 'Creating session...' : `Subscribe to ${plan.name}`}
</button>
{error && (
<div className="text-red-500">
<p>{error}</p>
{canRetry && <button onClick={handleSubscribe}>Retry</button>}
</div>
)}
</div>
);
}

Embedded Lemon Squeezy Checkout

function EmbeddedCheckout({ variantId }: { variantId: number }) {
const {
handleClick,
isLoading,
checkoutUrl,
isEmbedReady,
clearCheckout,
} = useCheckoutButton({
variantId,
embedded: true,
onPaymentSuccess: (res) => console.log('Payment complete!', res),
onClose: () => clearCheckout(),
dark: true,
});

return (
<div>
<button onClick={handleClick} disabled={isLoading}>
{isLoading ? 'Loading checkout...' : 'Purchase'}
</button>
{checkoutUrl && isEmbedReady && (
<div className="checkout-overlay">
{/* Lemon Squeezy embedded checkout renders here */}
</div>
)}
</div>
);
}

Configuration

Stripe Environment Variables

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_STRIPE_STANDARD_PRICE_ID=price_...
NEXT_PUBLIC_STRIPE_PREMIUM_PRICE_ID=price_...

All three must be set for Stripe checkout to be considered configured. The hook exports an isStripeConfigured boolean.

Multi-Currency Support

The useCreateCheckoutSession hook uses useCurrencyContext() to detect the user's currency and looks up currency-specific Stripe price IDs via getStripePriceConfig(). If no currency-specific price exists, it falls back to the plan's default price ID.

Retry Strategy

SettingValue
Max retries2
Retry conditionDoes not retry authentication errors
Retry delayExponential backoff, capped at 30 seconds

Edge Cases and Gotchas

  • Unauthenticated Users: useCreateCheckoutSession throws a CheckoutSessionError if user is null. It then redirects to /auth/signin via router.push.
  • Invalid Plan ID: If the plan ID does not match PaymentPlan.FREE, PaymentPlan.STANDARD, or PaymentPlan.PREMIUM, a CheckoutSessionError is thrown with a descriptive message.
  • Redirect vs Embedded: useCheckoutButton switches between Lemon Squeezy redirect and embedded modes based on the embedded parameter. The underlying hooks (useLemonSqueezyCheckoutWithRedirect and useLemonSqueezyEmbeddedCheckout) are both instantiated regardless, but only one is used.
  • Price Validation: handlePriceChange only accepts non-negative integers. Empty values set customPrice to undefined.
  • Cache Invalidation on Success: After a successful Stripe session creation, both subscriptions and user-subscription query caches are invalidated to keep subscription state fresh.
  • Automatic Redirect: useCreateCheckoutSession automatically sets window.location.href when the API returns a checkout URL. There is no way to prevent this redirect from within the hook.
  • usePlanGuard -- Check plan access before showing checkout options.
  • useMultiStepForm -- Build multi-step checkout wizards.
  • useToast -- Display payment success/error notifications.