PLAYGROUND connected · test-mode
Fork on GitHub
PREVIEW route · ch-inline flow · payment-intent
S stitch example app
← Back to plans
UPGRADING TO

Pro plan

For small teams

Pro · monthly
Renews monthly · cancel anytime
CHF 27.00
SubtotalCHF 27.00
VAT / Taxcalculated at confirmation
Total today CHF 27.00
    route · ch-inline

    How would you like to pay?

    you@stitch.example
    4242 4242 4242 4242 V M
    MM / YY
    CVC

    Powered by stripe via tenderlane · By paying, you agree to the Terms.

    RULES · routing.ts
    first-match-wins · 5 rules · 1 fallback
    fallback matched
    CONTEXT · the inputs your rules read
    country
    currency
    plan
    user.tier
    flags
    inline-checkout · ramp 64%
    3ds-always · ramp 100%
    localized-receipts · ramp 12%
    RESOLVED CONTEXT
     
    EVENT STREAM
    onRouteEvaluated · onStateChange
    context routing state adapter
    SOURCE · the code that wires this playground
    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;
    }