For small teams
Powered by stripe via tenderlane · By paying, you agree to the Terms.
import { TenderlaneProvider, TenderlaneCheckoutForm } from '@tenderlane/react';
import { createRulesRouter } from '@tenderlane/core';
import { stripeProvider } from '@tenderlane/stripe';
import { StripePaymentElement } from '@tenderlane/stripe/react';
import { rules, fallback } from './routing';
import { useAppContext } from './context';
const stripe = stripeProvider({
publishableKey: import.meta.env.STRIPE_PK,
serverEndpoint: '/api/pay',
});
export function Checkout({ plan }: { plan: Plan }) {
const context = useAppContext();
return (
<TenderlaneProvider
config={{
context,
providers: [stripe],
routing: createRulesRouter({ rules, fallback }),
}}
>
<OrderSummary plan={plan} />
<TenderlaneCheckoutForm
input={{
items: [{ sku: plan.id, quantity: 1 }],
successUrl: '/thanks',
}}
elements={{ stripe: StripePaymentElement }}
>
{({ canSubmit, submit, status }) => (
<button disabled={!canSubmit} onClick={submit}>
{status === 'submitting' ? 'Processing…' : 'Pay'}
</button>
)}
</TenderlaneCheckoutForm>
</TenderlaneProvider>
);
} import type { RoutingRule, Route } from '@tenderlane/core';
// First-match-wins. Rules are plain JSON — serializable, swappable at runtime.
export const rules: RoutingRule[] = [
{
id: 'ch-inline',
when: { country: 'CH', currency: 'chf' },
use: {
provider: 'stripe',
flow: 'payment-intent',
paymentMethods: ['card', 'twint'],
},
},
{
id: 'nl-ideal',
when: { country: 'NL', currency: 'eur' },
use: {
provider: 'stripe',
flow: 'payment-intent',
paymentMethods: ['card', 'ideal', 'sepa'],
},
},
{
id: 'de-klarna',
when: { country: 'DE', currency: 'eur' },
use: {
provider: 'stripe',
flow: 'payment-intent',
paymentMethods: ['card', 'sepa', 'klarna'],
},
},
{
id: 'br-pix',
when: { country: 'BR', currency: 'brl' },
use: {
provider: 'stripe',
flow: 'payment-intent',
paymentMethods: ['card', 'pix'],
},
},
{
id: 'gb-card',
when: { country: 'GB', currency: 'gbp' },
use: {
provider: 'stripe',
flow: 'payment-intent',
paymentMethods: ['card'],
},
},
];
export const fallback: Route = {
provider: 'stripe',
flow: 'checkout-session',
paymentMethods: ['card'],
}; import { createContext, useContext, useState } from 'react';
export interface AppContext {
country: string;
currency: 'usd' | 'eur' | 'chf' | 'brl' | 'jpy' | 'gbp';
plan: 'starter' | 'pro' | 'team';
tier: 'free' | 'starter' | 'pro' | 'team';
flag_inline: boolean;
flag_threeds: boolean;
flag_localized: boolean;
}
const AppCtx = createContext<AppContext | null>(null);
export function AppProvider({ children }: { children: React.ReactNode }) {
const [context] = useState<AppContext>({
country: 'CH',
currency: 'chf',
plan: 'pro',
tier: 'free',
flag_inline: true,
flag_threeds: true,
flag_localized: false,
});
return <AppCtx.Provider value={context}>{children}</AppCtx.Provider>;
}
export function useAppContext(): AppContext {
const context = useContext(AppCtx);
if (!context) throw new Error('AppProvider missing in tree');
return context;
}