PaymentGate
Provides the UI shell for collecting payment during booking. The actual Stripe Elements integration is injected via a render prop, allowing flexibility in payment method support (card, Apple Pay, Google Pay).
Install
Section titled “Install”npx thebookingkit add payment-gateimport { PaymentGate } from "@/components/payment-gate";import { PaymentElement, useStripe } from "@stripe/react-stripe-js";
export function BookingPayment({ clientSecret }: { clientSecret: string }) { const stripe = useStripe();
const handlePaymentSuccess = async (paymentIntentId: string) => { const { error } = await stripe!.confirmPayment({ elements: elements, redirect: "if_required", });
if (!error) { // Booking confirmed router.push("/confirmation"); } };
return ( <PaymentGate amountCents={2500} currency="USD" clientSecret={clientSecret} onPaymentSuccess={handlePaymentSuccess} renderPaymentElement={() => <PaymentElement />} /> );}export interface PaymentGateProps { /** Amount in smallest currency unit (e.g., cents) */ amountCents: number; /** ISO 4217 currency code (e.g., "USD") */ currency: string; /** Stripe client secret for the PaymentIntent */ clientSecret: string; /** Called when payment succeeds */ onPaymentSuccess: (paymentIntentId: string) => void; /** Called when payment fails */ onPaymentError?: (error: string) => void; /** Called when the user cancels/goes back */ onCancel?: () => void; /** Whether the payment is currently processing */ isProcessing?: boolean; /** Label for the pay button (default: "Pay {amount}") */ submitLabel?: string; /** Additional CSS class name */ className?: string; /** Inline styles */ style?: React.CSSProperties; /** * Render prop for the payment form element. * Integrators mount their Stripe Elements (or other payment UI) here. * If not provided, a placeholder is rendered. */ renderPaymentElement?: () => React.ReactNode;}Source
Section titled “Source”import React, { useState, useCallback } from "react";import { cn } from "../utils/cn.js";
/** Props for the PaymentGate component */export interface PaymentGateProps { /** Amount in smallest currency unit (e.g., cents) */ amountCents: number; /** ISO 4217 currency code (e.g., "USD") */ currency: string; /** Stripe client secret for the PaymentIntent */ clientSecret: string; /** Called when payment succeeds */ onPaymentSuccess: (paymentIntentId: string) => void; /** Called when payment fails */ onPaymentError?: (error: string) => void; /** Called when the user cancels/goes back */ onCancel?: () => void; /** Whether the payment is currently processing */ isProcessing?: boolean; /** Label for the pay button (default: "Pay {amount}") */ submitLabel?: string; /** Additional CSS class name */ className?: string; /** Inline styles */ style?: React.CSSProperties; /** * Render prop for the payment form element. * Integrators mount their Stripe Elements (or other payment UI) here. * If not provided, a placeholder is rendered. */ renderPaymentElement?: () => React.ReactNode;}
/** * Payment gate component that wraps a Stripe Payment Element (or similar). * * This component provides the UI shell for collecting payment during booking. * The actual Stripe Elements integration is injected via `renderPaymentElement` * since it requires `@stripe/react-stripe-js` which is an app-level dependency. * * Supports: Card, Apple Pay, Google Pay (via Stripe Payment Element). * * @example * ```tsx * <PaymentGate * amountCents={2500} * currency="USD" * clientSecret={clientSecret} * onPaymentSuccess={(id) => confirmBooking(id)} * renderPaymentElement={() => <PaymentElement />} * /> * ``` */export function PaymentGate({ amountCents, currency, clientSecret, onPaymentSuccess, onPaymentError, onCancel, isProcessing: externalProcessing, submitLabel, className, style, renderPaymentElement,}: PaymentGateProps) { const [internalProcessing, setInternalProcessing] = useState(false); const processing = externalProcessing ?? internalProcessing;
const formattedAmount = formatAmount(amountCents, currency); const buttonLabel = submitLabel ?? `Pay ${formattedAmount}`;
const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (processing) return;
setInternalProcessing(true);
try { // In a real integration, the parent would call stripe.confirmPayment() // using the clientSecret. This component signals readiness via the form submit. // The parent handles actual Stripe confirmation and calls onPaymentSuccess/onPaymentError. onPaymentSuccess(clientSecret); } catch (err) { const message = err instanceof Error ? err.message : "Payment failed"; onPaymentError?.(message); } finally { setInternalProcessing(false); } }, [processing, clientSecret, onPaymentSuccess, onPaymentError], );
return ( <div className={cn("tbk-payment-gate", className)} style={style} > <div className="tbk-payment-header"> <h3 className="tbk-payment-title">Payment</h3> <p className="tbk-payment-amount">{formattedAmount}</p> </div>
<form onSubmit={handleSubmit} className="tbk-payment-form"> <div className="tbk-payment-element"> {renderPaymentElement ? ( renderPaymentElement() ) : ( <div className="tbk-payment-placeholder"> <p>Payment element will be mounted here.</p> <p className="tbk-payment-hint"> Provide a <code>renderPaymentElement</code> prop to render your Stripe PaymentElement. </p> </div> )} </div>
<div className="tbk-payment-actions"> <button type="submit" className="tbk-button-primary" disabled={processing} > {processing ? "Processing..." : buttonLabel} </button>
{onCancel && ( <button type="button" className="tbk-button-secondary" onClick={onCancel} disabled={processing} > Cancel </button> )} </div> </form> </div> );}
/** Format an amount in cents to a display string */function formatAmount(amountCents: number, currency: string): string { const amount = amountCents / 100; try { return new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase(), }).format(amount); } catch { return `${currency.toUpperCase()} ${amount.toFixed(2)}`; }}