@headlesskits/react-headless-payments
v1.1.1
Published
Production-ready, headless payment integration for React. Stripe subscriptions made simple. Pairs with flask-headless-payments backend.
Maintainers
Readme
@headlesskit/react-payments
Production-ready, headless payment integration for React. Stripe subscriptions made simple. Pairs perfectly with flask-headless-payments backend.
✨ Features
- 💳 Stripe Integration - Checkout, subscriptions, billing portal
- 📊 Plan Management - List plans, show pricing, feature comparison
- 🔄 Subscription Lifecycle - Subscribe, upgrade, downgrade, cancel
- 🎨 Headless - Bring your own UI components
- 🎣 Composable Hooks -
useCheckout,usePlans,useSubscription,useBilling - 📦 Tree-Shakeable - Only import what you need
- 🔒 TypeScript - Full type safety
- ⚡ Zero Dependencies - Except React peer deps
📦 Installation
npm install @headlesskit/react-payments
# or
yarn add @headlesskit/react-payments
# or
pnpm add @headlesskit/react-payments🚀 Quick Start
1. Wrap your app with PaymentProvider
// app/layout.tsx (Next.js)
import { AuthProvider } from '@headlesskit/react-auth';
import { PaymentProvider } from '@headlesskit/react-payments';
export default function RootLayout({ children }) {
return (
<html>
<body>
<AuthProvider config={{ apiBaseUrl: process.env.NEXT_PUBLIC_API_URL! }}>
<PaymentProvider
config={{
apiBaseUrl: process.env.NEXT_PUBLIC_API_URL!,
apiPrefix: '/api/payments', // optional, this is default
}}
authTokenGetter={() => localStorage.getItem('auth_access_token')}
>
{children}
</PaymentProvider>
</AuthProvider>
</body>
</html>
);
}2. Display pricing plans
import { usePlans } from '@headlesskit/react-payments';
export function PricingTable() {
const { plans, loading, formatCurrency } = usePlans();
if (loading) return <div>Loading plans...</div>;
return (
<div className="pricing-grid">
{plans.map((plan) => (
<div key={plan.id} className="pricing-card">
<h3>{plan.name}</h3>
<p className="price">
{formatCurrency(plan.price)}/{plan.interval}
</p>
<p>{plan.description}</p>
<CheckoutButton plan={plan.id} />
</div>
))}
</div>
);
}3. Implement checkout
import { useCheckout } from '@headlesskit/react-payments';
export function CheckoutButton({ plan }: { plan: string }) {
const { createCheckout, loading } = useCheckout({
onSuccess: () => console.log('Redirecting to checkout...'),
onError: (error) => alert(error),
});
return (
<button
onClick={() => createCheckout(plan)}
disabled={loading}
>
{loading ? 'Loading...' : 'Subscribe'}
</button>
);
}4. Show subscription status
import { useSubscription } from '@headlesskit/react-payments';
export function SubscriptionStatus() {
const { subscription, hasSubscription, isOnTrial } = useSubscription();
if (!hasSubscription) {
return <div>No active subscription</div>;
}
return (
<div>
<h3>Current Plan: {subscription.plan_name}</h3>
<p>Status: {subscription.status}</p>
{isOnTrial && <p>Trial ends: {subscription.trial_end}</p>}
</div>
);
}5. Add billing portal link
import { useBilling } from '@headlesskit/react-payments';
export function ManageSubscriptionButton() {
const { openBillingPortal, loading } = useBilling();
return (
<button onClick={openBillingPortal} disabled={loading}>
Manage Subscription
</button>
);
}📚 Core Concepts
PaymentProvider
The main component that provides payment context to your app.
<PaymentProvider
config={{
apiBaseUrl: 'https://api.myapp.com',
apiPrefix: '/api/payments', // default
successUrl: '/payment/success', // default
cancelUrl: '/payment/cancel', // default
portalUrl: '/dashboard/billing', // default
currency: 'usd', // default
locale: 'en-US', // default
}}
// Auth integration
authTokenGetter={() => getAccessToken()}
// Lifecycle callbacks
onCheckoutSuccess={(url) => console.log('Checkout created:', url)}
onCheckoutError={(error) => toast.error(error)}
onSubscriptionChange={(subscription) => {
// React to subscription changes
analytics.track('subscription_changed', { plan: subscription?.plan });
}}
>
{children}
</PaymentProvider>Hooks
usePayments
Main hook for all payment functionality.
const {
// State
plans,
plansLoading,
subscription,
subscriptionLoading,
hasSubscription,
isOnTrial,
// Actions
createCheckout,
openPortal,
cancelSubscription,
upgradeSubscription,
refreshSubscription,
refreshPlans,
// Utilities
formatCurrency,
} = usePayments();usePlans
Focused hook for pricing plans.
const { plans, loading, refreshPlans, formatCurrency } = usePlans();useSubscription
Focused hook for subscription management.
const {
subscription,
loading,
hasSubscription,
isOnTrial,
refresh,
cancel,
upgrade,
} = useSubscription();useCheckout
Hook for checkout operations with loading and error states.
const { createCheckout, loading, error } = useCheckout({
onSuccess: (url) => console.log('Success'),
onError: (error) => console.error('Error'),
});
// Use it
await createCheckout('pro_plan', 14); // 14-day trialuseBilling
Hook for billing portal access.
const { openBillingPortal, loading } = useBilling();🎯 Usage Examples
Pricing Table with Features
import { usePlans, useCheckout } from '@headlesskit/react-payments';
export function PricingTable() {
const { plans, formatCurrency } = usePlans();
const { createCheckout, loading } = useCheckout();
return (
<div className="grid grid-cols-3 gap-4">
{plans.map((plan) => (
<div key={plan.id} className="border rounded-lg p-6">
<h3 className="text-2xl font-bold">{plan.name}</h3>
<p className="text-4xl font-bold my-4">
{formatCurrency(plan.price)}
<span className="text-sm text-gray-500">/{plan.interval}</span>
</p>
{plan.features && (
<ul className="space-y-2 mb-6">
{plan.features.map((feature, i) => (
<li key={i} className="flex items-center">
<CheckIcon className="w-5 h-5 text-green-500 mr-2" />
{feature}
</li>
))}
</ul>
)}
<button
onClick={() => createCheckout(plan.id)}
disabled={loading}
className="w-full bg-blue-600 text-white py-2 rounded"
>
{loading ? 'Processing...' : 'Subscribe'}
</button>
</div>
))}
</div>
);
}Subscription Management Page
import { useSubscription, useBilling } from '@headlesskit/react-payments';
export function SubscriptionManagementPage() {
const { subscription, hasSubscription, cancel } = useSubscription();
const { openBillingPortal } = useBilling();
if (!hasSubscription) {
return (
<div>
<h2>No Active Subscription</h2>
<Link href="/pricing">View Plans</Link>
</div>
);
}
return (
<div>
<h2>Your Subscription</h2>
<div className="subscription-details">
<p><strong>Plan:</strong> {subscription.plan_name}</p>
<p><strong>Status:</strong> {subscription.status}</p>
<p><strong>Renews:</strong> {new Date(subscription.current_period_end).toLocaleDateString()}</p>
{subscription.is_on_trial && (
<p className="text-blue-600">
Trial ends: {new Date(subscription.trial_end!).toLocaleDateString()}
</p>
)}
</div>
<div className="actions">
<button onClick={openBillingPortal}>
Update Payment Method
</button>
<button onClick={cancel} className="text-red-600">
Cancel Subscription
</button>
</div>
</div>
);
}Plan Upgrade Flow
import { useSubscription, usePayments } from '@headlesskit/react-payments';
export function UpgradePlanModal() {
const { subscription } = useSubscription();
const { plans, upgradeSubscription } = usePayments();
const handleUpgrade = async (newPlan: string) => {
try {
await upgradeSubscription(newPlan);
alert('Plan upgraded successfully!');
} catch (error) {
alert('Upgrade failed');
}
};
const availableUpgrades = plans.filter(
(plan) => plan.id !== subscription?.plan
);
return (
<div>
<h3>Upgrade Your Plan</h3>
{availableUpgrades.map((plan) => (
<div key={plan.id}>
<h4>{plan.name}</h4>
<button onClick={() => handleUpgrade(plan.id)}>
Upgrade to {plan.name}
</button>
</div>
))}
</div>
);
}Gated Feature Component
import { useSubscription } from '@headlesskit/react-payments';
import Link from 'next/link';
export function PremiumFeature({ children }: { children: React.ReactNode }) {
const { hasSubscription, subscription } = useSubscription();
const isPro = subscription?.plan === 'pro' || subscription?.plan === 'enterprise';
if (!hasSubscription || !isPro) {
return (
<div className="blur-sm relative">
{children}
<div className="absolute inset-0 flex items-center justify-center bg-white/80">
<div className="text-center">
<h3>Premium Feature</h3>
<p>Upgrade to Pro to unlock this feature</p>
<Link href="/pricing">
<button className="bg-blue-600 text-white px-6 py-2 rounded">
View Plans
</button>
</Link>
</div>
</div>
</div>
);
}
return <>{children}</>;
}🔌 Backend Integration
Works seamlessly with flask-headless-payments:
# Backend setup (Flask)
from flask_headless_payments import PaymentSvc
payments = PaymentSvc(
app,
user_model=User,
plans={
'free': {
'name': 'Free',
'price_id': None,
},
'pro': {
'name': 'Pro',
'price_id': 'price_xxx',
},
},
url_prefix='/api/payments' # Match your frontend config
)// Frontend setup
<PaymentProvider
config={{
apiBaseUrl: process.env.NEXT_PUBLIC_API_URL,
apiPrefix: '/api/payments', // Must match backend
}}
/>📖 API Reference
PaymentProvider Props
interface PaymentProviderProps {
children: ReactNode;
config: {
apiBaseUrl: string; // Required
apiPrefix?: string; // Default: '/api/payments'
successUrl?: string; // Default: '/payment/success'
cancelUrl?: string; // Default: '/payment/cancel'
portalUrl?: string; // Default: '/dashboard/billing'
currency?: string; // Default: 'usd'
locale?: string; // Default: 'en-US'
debug?: boolean;
};
authTokenGetter?: () => string | null; // Required for authenticated requests
onCheckoutSuccess?: (url: string) => void;
onCheckoutError?: (error: string) => void;
onSubscriptionChange?: (subscription: Subscription | null) => void;
}Plan Type
interface Plan {
id: string;
name: string;
description?: string;
price: number;
currency: string;
interval: 'month' | 'year';
stripe_price_id: string;
features?: string[];
limits?: Record<string, number>;
}Subscription Type
interface Subscription {
id: string;
plan: string;
plan_name?: string;
status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid';
stripe_subscription_id: string;
current_period_end: string;
cancel_at_period_end: boolean;
is_subscribed: boolean;
is_on_trial: boolean;
days_until_renewal?: number;
}🤝 Contributing
Contributions are welcome! Please see CONTRIBUTING.md.
📄 License
MIT © HeadlessKit Contributors
🔗 Related Packages
- flask-headless-payments - Backend companion
- @headlesskit/react-auth - Authentication integration
💬 Support
Built with ❤️ by the HeadlessKit team
