FloPayFloPay
Guides

FloPayCheckout

The recommended way to add payments — a single component that handles everything.

FloPayCheckout Guide

FloPayCheckout is the fastest way to integrate FloPay. One component handles session fetching, Stripe initialization, card fields, wallets, PayPal, 3DS, and checkout modes — no manual wiring needed.

Quick Start

The recommended integration is one-shot: pass a createSession payload and FloPayCheckout creates the session and renders the form in a single step — no separate backend route to build, call, and pass a sessionId back to the client.

import { FloPayCheckout } from '@flopay/react';
import { configureFlopay } from '@flopay/shared';

// Call once at app startup (layout.tsx or _app.tsx)
configureFlopay({ environment: 'production' });

function CheckoutPage() {
  return (
    <FloPayCheckout
      createSession={{
        clientId: 'your-client-id',
        currency: 'EUR',
        products: [{ code: 'omni-ai-booster' }],
        account: {
          userId: 'user_123',
          email: 'customer@example.com',
          firstName: 'Jane',
          lastName: 'Doe',
        },
        successUrl: '/success',
        cancelUrl: '/cancel',
      }}
      onComplete={(result) => {
        if (result.status === 'succeeded') {
          window.location.href = '/success';
        }
      }}
      onError={(err) => console.error(err.message)}
    />
  );
}

That's it. The component automatically:

  1. Creates the checkout session from your createSession payload (no backend route required)
  2. Reads gateways.stripe.publishableKey to initialize card fields and every payment method enabled on the connected Stripe account
  3. Reads gateways.paypal to decide which PayPal path to render:
    • When gateways.paypal is present: loads the PayPal JS SDK directly (see Direct PayPal guide)
    • When null or missing: renders PayPal via Stripe's ExpressCheckoutElement as fallback (see PayPal via Stripe)
    • PayPal never renders twice
  4. Uses each gateway entry's environment ('stage' or 'production') to pick sandbox vs live credentials per gateway
  5. Renders card fields, the Stripe-enabled payment methods configured on the connected Stripe account (Apple Pay, Google Pay, Cash App, Klarna, Afterpay, iDEAL, Bancontact, etc.), and PayPal
  6. Handles 3DS authentication
  7. Calls onComplete when payment succeeds

See Inline Session Creation for the full createSession payload, currency requirements, and email handling.

Already have a session ID?

If you create the session on your backend (for example to keep pricing or entitlement logic server-side), pass the returned sessionId instead of createSession:

<FloPayCheckout sessionId={sessionId} onComplete={handleSuccess} />

Every option on this page works the same whether you pass createSession or sessionId. The examples below use sessionId for brevity.

Environment Setup

FloPayCheckout needs to know which billing API to call. Choose one:

import { configureFlopay } from '@flopay/shared';
configureFlopay({ environment: 'production' });

Call this once at app startup, for example in app/layout.tsx or _app.tsx.

Option B: Explicit prop

<FloPayCheckout
  sessionId={sessionId}
  billingApiUrl="https://api.flopay.com"
  onComplete={handleSuccess}
/>

Checkout Modes

Full Mode (default)

Shows the complete payment form — card fields, wallets, PayPal.

<FloPayCheckout sessionId={sessionId} onComplete={handleSuccess} />

Confirm Mode

Uses a saved payment method. Shows a single "Confirm Purchase" button. Falls back to full mode if payment fails.

<FloPayCheckout
  sessionId={sessionId}
  checkoutMode="confirm"
  onComplete={handleSuccess}
/>

Customize the confirm button:

<FloPayCheckout
  sessionId={sessionId}
  checkoutMode="confirm"
  renderConfirmButton={({ onConfirm, isProcessing }) => (
    <button onClick={onConfirm} disabled={isProcessing}>
      {isProcessing ? 'Working...' : 'Buy Now'}
    </button>
  )}
  onComplete={handleSuccess}
/>

Auto Mode

Automatically submits with a saved payment method — no user interaction. Falls back to full mode if it fails.

<FloPayCheckout
  sessionId={sessionId}
  checkoutMode="auto"
  onComplete={handleSuccess}
  onSessionCompleted={(successUrl) => {
    // Session was already completed (e.g. page reload after success)
    window.location.href = successUrl;
  }}
/>

Layout Modes

Default Layout

All payment methods visible together: wallets on top, divider, then card fields below.

Buttons Layout

Payment methods shown as stacked buttons. Clicking "Credit / Debit Card" expands into the card form with a back button and title.

<FloPayCheckout
  sessionId={sessionId}
  layout="buttons"
  onComplete={handleSuccess}
/>

Theme Bundles

Pick a bundled theme and the SDK styles both the Stripe-rendered fields and the React-rendered wrapper, submit, and inputs:

// Modern light — Inter, soft shadows, generous spacing
<FloPayCheckout sessionId={id} layout="buttons" theme="modern-light" />

// Bold dark — saturated FloPay blue on a dark surface
<FloPayCheckout sessionId={id} layout="buttons" theme="bold-dark" />

// Glass dark — translucent surfaces over a blue gradient
<FloPayCheckout sessionId={id} layout="buttons" theme="glass-dark" />

Available theme values: 'classic' | 'modern-light' | 'modern-dark' | 'bold-light' | 'bold-dark' | 'glass-light' | 'glass-dark'. See the Theming guide for the full reference and resolution precedence.

Custom Styles

Override individual elements with buttonsStyles:

<FloPayCheckout
  sessionId={sessionId}
  layout="buttons"
  buttonsStyles={{
    cardButton: { borderRadius: '12px', border: '2px solid #4A49FF' },
    cardButtonFontSize: '1rem',
    cardFormContainer: { backgroundColor: '#f8f7ff' },
    cardInputBorder: '#c4c3ff',
    cardInputColor: '#1a1a2e',
    cardInputPlaceholderColor: '#9ca3af',
    submitButton: { backgroundColor: '#2d2ccc', borderRadius: '12px' },
    title: { color: '#2d2ccc' },
  }}
  onComplete={handleSuccess}
/>

Custom Card Button Content

Use cardButtonContent to replace the default card button body with your own React content:

<FloPayCheckout
  sessionId={sessionId}
  layout="buttons"
  cardButtonContent={
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: '0.75rem',
        width: '100%',
      }}
    >
      <span style={{ fontWeight: 700 }}>Pay by card</span>
      <span style={{ fontSize: '0.75rem', opacity: 0.7 }}>
        Visa, Mastercard, Amex
      </span>
      <span style={{ marginLeft: 'auto', fontSize: '0.7rem' }}>Secure</span>
    </div>
  }
  onComplete={handleSuccess}
/>

Use cardButtonContent for the inner content and buttonsStyles.cardButton for the outer button container styles.

Buttons Layout Header Slots

Use cardBackButtonContent and cardTitleContent to replace the default "Go back" and "Secure card checkout" text in the expanded card form header. Pass '' when you want to remove the text completely.

<FloPayCheckout
  sessionId={sessionId}
  layout="buttons"
  cardBackButtonContent=""
  cardTitleContent="Enter card details"
  onComplete={handleSuccess}
/>

cardTitleContent also replaces the standalone title in the default card form layout.

For dark backgrounds, the bundled bold-dark / glass-dark themes already style the Stripe Element text inside the iframe. Override individual fields with cardInputColor, cardInputPlaceholderColor, and cardInputBackground when needed:

<FloPayCheckout
  sessionId={sessionId}
  layout="buttons"
  theme="bold-dark"
  buttonsStyles={{
    cardInputColor: '#f9fafb',
    cardInputPlaceholderColor: '#6b7280',
    cardInputBackground: '#1f2937',
    nameInput: { backgroundColor: '#1f2937', color: '#f9fafb' },
  }}
  onComplete={handleSuccess}
/>

See the full ButtonsLayoutStyles reference for all available properties.

Inline Session Creation (One-Shot)

This is the recommended flow shown in the Quick Start — pass createSession and skip the backend API route entirely. The component creates the session and renders the checkout in one step:

<FloPayCheckout
  layout="buttons"
  createSession={{
    clientId: 'your-client-id',
    currency: 'EUR',
    products: [{ code: 'omni-ai-booster' }],
    account: {
      userId: 'user_123',
      email: 'customer@example.com',
      firstName: 'John',
      lastName: 'Doe',
    },
    successUrl: '/success',
    cancelUrl: '/cancel',
  }}
  onComplete={(result) => window.location.href = '/success'}
/>

No sessionId needed — the component creates the session, initializes the payment provider, and renders the form automatically.

For embedded checkout, include createSession.account.email in the initial payload. If you only have a temporary value such as test@email.com, pass it first and then replace it in onBeforeButtonClick before the card flow continues.

Session-level currency is required. The SDK throws FloPayError({ type: 'validation_error', code: 'CurrencyRequired' }) synchronously when no currency can be resolved from the session or any per-line currency. Set createSession.currency at the top of the payload for the cleanest behavior.

Tracking Button Clicks

Fire GTM events when users interact with payment buttons:

<FloPayCheckout
  sessionId={sessionId}
  layout="buttons"
  onButtonClick={(method) => {
    window.dataLayer?.push({
      event: 'initiate_checkout',
      payment_method: method,
    });
  }}
  onComplete={handleSuccess}
/>

method values: 'card', 'paypal', 'apple_pay', 'google_pay'.

Before Card Button Click

Use onBeforeButtonClick when you need to do async work before the credit card button continues, such as confirming checkout details or replacing a temporary email address:

<FloPayCheckout
  layout="buttons"
  createSession={{
    clientId: 'your-client-id',
    currency: 'EUR',
    products: [{ code: 'product-1' }],
    account: {
      userId: 'user_1',
      email: 'test@email.com',
    },
    successUrl: '/success',
    cancelUrl: '/cancel',
  }}
  onBeforeButtonClick={async ({ method, createSession }) => {
    if (method !== 'card') return;

    const email = await openEmailCaptureModal({
      initialEmail: createSession?.account.email ?? '',
    });
    if (!email) return false;

    return {
      account: { email },
    };
  }}
  onComplete={handleSuccess}
/>

onBeforeButtonClick is credit card only. It runs only for the "Credit / Debit Card" button in layout="buttons". It does not run for PayPal, Apple Pay, or Google Pay. If you need required data before every payment method, collect it before rendering FloPayCheckout.

Returning false cancels the card click. Throwing routes the error through onError. When createSession is used, the returned patch refreshes the card flow with the merged session draft. createSession.account.email should already be present when the embedded checkout renders; use this hook to update it, not to skip it.

Decline Events

Use onDecline when you want GTM or analytics events for declined payments, failed 3DS, PayPal cancellation, or wallet dismissal:

<FloPayCheckout
  sessionId={sessionId}
  onDecline={(decline) => {
    window.dataLayer?.push({
      event: 'checkout_decline',
      ...decline,
    });
  }}
  onComplete={handleSuccess}
/>

Payment Methods

Payment methods are toggled at the gateway level with two props: showStripe and showPayPal. Which individual methods appear inside the Stripe gateway (cards, Apple Pay, Google Pay, Cash App, Klarna, Afterpay, iDEAL, Bancontact, etc.) is controlled from your Stripe dashboard — see Stripe payment methods.

Stripe

Enabled by default. Renders card fields plus every payment method enabled on the connected Stripe account. Apple Pay and Google Pay only paint on supported devices/browsers; Stripe-enabled local methods (Cash App, Klarna, Afterpay, iDEAL, Bancontact, etc.) appear when the buyer's locale and currency match the method's eligibility rules.

// Hide the entire Stripe gateway — cards, wallets, and all Stripe-enabled payment methods
<FloPayCheckout
  sessionId={sessionId}
  showStripe={false}
  onComplete={handleSuccess}
/>

Wallet buttons depend on Stripe domain registration. Apple Pay also needs the Apple association file. See the Apple Pay Setup and Google Pay Setup guides.

PayPal

Enabled by default. Routes to the direct PayPal path when the session response includes gateways.paypal, otherwise falls back to PayPal via Stripe. Handles redirects automatically.

// Hide the entire PayPal gateway (both direct and Stripe-rendered paths)
<FloPayCheckout sessionId={sessionId} showPayPal={false} onComplete={handleSuccess} />

AVS (Address Verification)

Enable AVS to collect the user's country and postal/ZIP code. When enabled, billing_details are passed to Stripe's createPaymentMethod() so Stripe can run postal code and address verification checks automatically.

Basic Setup

<FloPayCheckout
  sessionId={sessionId}
  enableAVS
  onComplete={handleSuccess}
/>

The country dropdown defaults to the session's customer.country value (typically resolved from the user's IP by your backend). If no country is provided, it defaults to US.

Country from GEO/IP Lookup

Pass the user's country in the session creation params. The SDK pre-fills the dropdown:

<FloPayCheckout
  createSession={{
    clientId: 'your-client-id',
    currency: 'EUR',
    products: [{ code: 'product-1' }],
    account: {
      userId: 'user_1',
      email: 'user@example.com',
      country: 'GB', // resolved from user's IP
    },
    successUrl: '/success',
    cancelUrl: '/cancel',
  }}
  enableAVS
  layout="buttons"
  onComplete={handleSuccess}
/>

The dropdown shows "United Kingdom" and the postal code label says "Postcode" instead of "ZIP Code".

Dynamic Labels

The postal code field label adapts to the selected country:

CountryLabel
USZIP Code
GB, AU, NZPostcode
CAPostal Code
IEEircode
All othersPostal Code

AVS Field Layout

By default, country and postal code sit side-by-side in a row. Switch to stacked:

<FloPayCheckout
  sessionId={sessionId}
  enableAVS
  avsLayout="column"
  onComplete={handleSuccess}
/>

Theming AVS Fields

AVS fields inherit from the card input styles. Override individually with countrySelect and zipInput in buttonsStyles:

<FloPayCheckout
  sessionId={sessionId}
  enableAVS
  layout="buttons"
  theme="bold-dark"
  buttonsStyles={{
    countrySelect: { backgroundColor: '#1f2937', color: '#f9fafb' },
    zipInput: { backgroundColor: '#1f2937', color: '#f9fafb' },
  }}
  onComplete={handleSuccess}
/>

How It Works

  1. User selects country and enters postal code in the form
  2. On submit, billing_details (name + address with country and postal_code) are passed to Stripe's createPaymentMethod()
  3. Stripe runs AVS checks automatically when billing details are present
  4. The accountData.zip and accountData.country are also sent to your backend in the process request
  5. Configure Stripe Dashboard → Radar → Rules to block or allow based on AVS results (e.g., "Block if postal code check fails")

AVS is performed by Stripe at payment confirmation time. The SDK sends the billing details — your Stripe Radar rules determine whether to block, allow, or flag based on the verification result. No backend code changes are needed for basic AVS.

Error Handling

<FloPayCheckout
  sessionId={sessionId}
  onError={(err) => {
    // err.type: 'validation_error' | 'api_error' | 'authentication_error' | ...
    // err.message: human-readable error message
    console.error(`Payment failed: ${err.message}`);
    showToast(err.message);
  }}
  onComplete={handleSuccess}
/>

Suppress the built-in error UI and handle it yourself:

<FloPayCheckout
  sessionId={sessionId}
  error={() => <></>}
  onError={(err) => setMyError(err.message)}
  onComplete={handleSuccess}
/>

Custom Loading State

<FloPayCheckout
  sessionId={sessionId}
  loading={<MySkeletonLoader />}
  onComplete={handleSuccess}
/>

Full Example

See the FloPayCheckout Example for a complete checkout page with order summary, discount timer, and all props configured.

Next Steps

On this page