npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flopay/react

v1.8.2

Published

React bindings for the FloPay SDK. Provides a context provider, drop-in checkout form components, individual element components, and hooks.

Readme

@flopay/react

React bindings for the FloPay SDK. Provides a context provider, drop-in checkout form components, individual element components, and hooks.

Installation

pnpm add @flopay/react @flopay/js @flopay/shared react react-dom

Peer dependencies: react >= 18.0.0, react-dom >= 18.0.0.

Environment Setup

Configure which billing API the SDK uses. Choose one:

# Option A: Environment variable (recommended)
# .env.production
NEXT_PUBLIC_FLOPAY_ENV=production

# .env.development
NEXT_PUBLIC_FLOPAY_ENV=staging
// Option B: Call at app startup
import { configureFlopay } from '@flopay/shared';
configureFlopay({ environment: 'production' });

If neither is set, the SDK defaults to staging.

Quick Start

Recommended: FloPayCheckout

The simplest integration — one component, one prop:

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

function CheckoutPage() {
  return (
    <FloPayCheckout
      sessionId="sess_abc123"
      onComplete={(result) => {
        window.location.href = '/success';
      }}
      onError={(err) => console.error(err)}
    />
  );
}

FloPayCheckout automatically fetches the session, initializes the correct payment providers, and renders a SplitCardForm with the hosted vault card widget plus the session's supported wallets, APMs, and PayPal. Customer data (email, userId, name) is injected from the session.

Merchant instrument feed

Use onInstrument on FloPayCheckout or FloPayProvider to forward a stable, privacy-safe checkout funnel to your own analytics. The callback receives the exported FloInstrumentEvent discriminated union; schemaVersion is always 1, and gateway is included as stripe or paypal only when the event is attributable to that payment provider. Hosted-vault events may omit gateway.

import type { FloInstrumentEvent } from '@flopay/react';

function forwardCheckoutInstrument(event: FloInstrumentEvent) {
  window.analytics?.track(event.name, event);
}

<FloPayCheckout sessionId={sessionId} onInstrument={forwardCheckoutInstrument} />;

Lifecycle names are checkout_mount, sdk_loaded, form_rendered, card_expanded, tokenize, process_attempt, and 3ds_challenge. checkout_mount, sdk_loaded, form_rendered, and card_expanded arrive at most once per logical checkout. tokenize, process_attempt, and 3ds_challenge may repeat for each attempt. checkout_error adds one of four phases: session_create, sdk_load, process, or wallets.

The callback is an allowlisted projection with no card data, tokens, provider object IDs, secrets, or raw PII. A throwing onInstrument consumer never breaks checkout. This merchant-owned feed is independent of the Flo-owned backend telemetry setting, so telemetry={false} does not disable it.

Checkout Modes

FloPayCheckout supports three checkout modes matching the billing API's checkoutMode field:

  • full (default) — Shows the hosted card widget plus supported non-card methods.
  • confirm — Hides the payment form and shows a "Confirm Purchase" button. Uses a saved payment method on the backend. When the saved card needs re-authentication, the 3DS challenge runs in place and the same payment attempt completes — the button is not replaced by the card form.
  • auto — Auto-submits with a saved payment method after the session loads. Falls back to full mode on failure.
// Confirm mode — one-click purchase with saved card
<FloPayCheckout
  sessionId="sess_abc123"
  checkoutMode="confirm"
  confirmLabel="Complete Purchase"
  onComplete={(result) => router.push('/success')}
/>

// Auto mode — instant checkout, falls back to full form
<FloPayCheckout
  sessionId="sess_abc123"
  checkoutMode="auto"
  onComplete={(result) => router.push('/success')}
  onSessionCompleted={(successUrl) => router.push(successUrl)}
/>

The mode can also be set on the session itself via the billing API's checkoutMode field. The checkoutMode prop overrides the session value.

Secure saved-card setup

Use FloPayCardSetup to let a customer add or verify a card without creating a purchase or charge. Your trusted server must first call the merchant-authenticated card-setup endpoint and pass only its opaque sessionId and bound nonce to the browser. Never send Client Basic credentials, OAuth tokens, or a customer identifier to this component.

import { FloPayCardSetup } from '@flopay/react';

function AddCard({ sessionId, nonce }: { sessionId: string; nonce: string }) {
  return (
    <FloPayCardSetup
      sessionId={sessionId}
      nonce={nonce}
      onComplete={({ paymentMethod }) => {
        // Display-only when supplied. Re-list from the server if absent during
        // a mixed-version backend rollout.
        console.log(paymentMethod?.brand, paymentMethod?.lastFour);
      }}
      onDecline={({ message }) => showCardError(message)}
      onValidation={(message) => showCardValidation(message)}
      onError={(error) => {
        if (error.retryable) showRetry();
      }}
      onCancel={() => closeAddCard()}
    />
  );
}

The component reads and validates the setup session before injecting FloPay's hosted PCI card widget. It refuses purchase sessions, zero-amount non-setup sessions, non-zero setup sessions, and setup sessions containing products. Bank authentication is shown through the existing hosted challenge and does not count as success while pending. Completion fires only after verification says the card is usable; decline, validation, retryable technical failure, and unmount cancellation remain separate outcomes.

Submission feedback matches checkout: the same processing overlay covers the form from the buyer's submit ("SAVING CARD...") and flips to "CARD SAVED" (held briefly before onComplete) or "CARD NOT SAVED" with the decline message (cleared automatically so the buyer can retry in place).

Saved-card listing, setup-session creation, and deletion remain authenticated merchant REST calls. Replacement is composition: complete setup for the new card, then ask your server to delete the old card. FloPayCardSetup deliberately has no customer-id, merchant-credential, list, delete, purchase, or Stripe Elements prop.

Prefer importing it from the dedicated subpath on pages that never run a checkout:

import { FloPayCardSetup } from '@flopay/react/card-setup';

The main entry re-exports the component for back-compat, but its module graph eagerly loads Stripe.js (a deliberate checkout preload); the card-setup entry's graph never reaches Stripe, so saved-card pages stay free of Stripe network activity by construction.

Expired Sessions

When FloPayCheckout or FloPayAutomaticPaymentButton loads a checkout session whose status is expired, the component calls onError with a FloPayError so your app can route the customer into a recovery flow.

The error has:

  • message: 'Checkout session has expired.'
  • type: 'api_error'
  • code: 'checkout_session_expired'

Use this code to create or request a fresh checkout session, then send the customer back through checkout:

<FloPayCheckout
  sessionId={sessionId}
  onComplete={() => router.push('/success')}
  onError={(error) => {
    if (error.code === 'checkout_session_expired') {
      router.push('/checkout/expired');
      return;
    }

    console.error(error);
  }}
/>

Automatic Payment Buttons

Use FloPayAutomaticPaymentButton when you want a single reusable button that:

  • creates an auto checkout session inline or reuses an existing sessionId
  • lets the backend resolve and charge the customer's most recently vaulted payment method (across providers)
  • shows the shared processing / success / failure modal
  • emits the same success and decline events without rendering a full checkout form

The button accepts either:

  • sessionId
  • or the same session-creation props you would normally post to the backend: clientId, items, subscriptions, account, successUrl, cancelUrl, couponCodes, tagsData, utmMetadata

It accepts the same theme prop as FloPayCheckout and SplitCardForm (see Theming below) — the value is forwarded to the fallback FloPayCheckout modal so the entire flow stays visually consistent. When the saved-payment charge can't complete silently, the button opens the standard FloPayCheckout inline with the same theme already applied.

Removed in this version: the legacy paymentMethodId and checkoutMethod props are now @deprecated and silently ignored. The backend's auto-checkout (createSingle) looks up the customer's latest vaulted payment method via getLatestByUserId and rebinds the session's gateway to match, so the SDK no longer hand-picks a PM or a provider. Existing integrations that still pass these props continue to work — they have no effect.

import { FloPayAutomaticPaymentButton } from '@flopay/react';

function UpsellButton() {
  return (
    <FloPayAutomaticPaymentButton
      clientId="your-client-id"
      account={{ userId: 'user_1', email: '[email protected]' }}
      items={[
        {
          providerItemId: 'upsell_1',
          providerItemName: 'AI Supercharger Pack',
          totalAmount: 249,
          overrideAmount: 80,
          currency: 'EUR',
          quantity: 1,
        },
      ]}
      successUrl={`${window.location.origin}/success`}
      cancelUrl={`${window.location.origin}/success`}
      theme="bold-dark"
      onClick={() => {
        window.dataLayer?.push({ event: 'automatic_payment_button_click' });
      }}
      onSuccess={() => {
        console.log('automatic payment succeeded');
      }}
      onError={(error) => {
        console.error(error.message);
      }}
      onDecline={(decline) => {
        console.log('automatic payment declined', decline);
      }}
    >
      Purchase Item
    </FloPayAutomaticPaymentButton>
  );
}

children render inside the button, so you can treat them as slot content. If omitted, the default buttons-layout card content is used.

Wallet Buttons & alternative payment methods

The payment-method list is now gateway-driven: the billing API ships gateways.stripe.enabledPaymentMethods per session, derived from the Stripe Payment Method Configurations enabled on the merchant account. The SDK partitions that list into two regions inside SplitCardForm:

  • ExpressCheckoutElementapple_pay, google_pay, paypal, link, amazon_pay, klarna (everything supported as a native big button).
  • PaymentElement (accordion) — every other enabled method, e.g. cashapp, affirm, ideal, bancontact, sepa_debit.

card is a capability marker, not a Stripe-rendered method. It keeps the Credit / Debit Card path available and tells SDK 1.4.9+ that the hosted vault form can be requested lazily when session.vault is absent.

Enabling Cash App Pay (or any future Stripe method) in the Stripe dashboard is the entire integration change — no SDK redeploy or consumer code update is required. Wallets only render on supported devices regardless of dashboard state (Apple Pay on Safari/macOS/iOS, Google Pay on Chrome).

Payment-logo CDN and CSP: APM brand logos load from the versioned, FloPay-owned URL https://cdn.flopay.com/sdk-logos/v1/. Plain absolute HTTPS URLs work in Vite with no SDK middleware or optimizeDeps override, and retain the same behavior in webpack 5, Rollup, Next, and esbuild. Logos stay out of the package and JavaScript bundle; card-only and vault checkouts never request one. Merchants with a strict Content Security Policy must allow the host in the image directive, for example img-src 'self' https://cdn.flopay.com;. A failed or CSP-blocked logo request falls back to the method's existing inline monogram, so the tile remains branded without another network request.

In layout="buttons", the Credit / Debit Card button always opens the inline card form. External wallets and PayPal keep their own lifecycle: buyer cancellation silently returns to the payment-method chooser with all eligible options available, while post-click technical failures, popup blocking, or a provider surface returning focus without a terminal callback restore the chooser, focus a visible retry control for the failed method, show safe method-specific copy such as We couldn't open Google Pay. Try again or choose another payment method., and call onError with a structured FloPayError. Passive pre-click load or eligibility failures still collapse only the unavailable method and do not show a checkout error.

Toggle the whole Stripe region or the PayPal region independently with the two gateway-level props:

<FloPayCheckout
  sessionId="sess_abc123"
  showStripe={true}  // default: true — Stripe wallets/APMs only
  showPayPal={true}  // default: true — DirectPayPal when gateway present,
                     //                  Stripe-rendered PayPal otherwise
  onComplete={handleSuccess}
/>

Setting both to false (with no PayPal gateway configured) is treated as a bootstrap-time validation error — onError fires with a FloPayError({ type: 'validation_error' }) so the misconfiguration surfaces during integration instead of silently rendering an empty form.

Deprecated props

showApplePay, showGooglePay, and directPaypal are still accepted but emit a one-time console.warn when supplied alongside the new enabledPaymentMethods payload. They are no-ops once the backend ships the field — Apple Pay / Google Pay are dashboard-controlled at Stripe, and direct PayPal is auto-resolved from gateways.paypal on the session response. The legacy props will be removed in 2.0.

Theming

FloPayCheckout, FloPayAutomaticPaymentButton, SplitCardForm, and FloPayCardSetup accept a single theme prop that maps to a coherent {appearance, buttonsLayout} bundle in @flopay/shared's THEMES map. (FloPayCardSetup styles only the hosted vault widget — there are no surrounding Stripe Elements on that surface — and unlike the checkout components it defaults to 'modern-light' rather than 'classic'.) One value styles non-card Stripe Elements, the React-rendered wrapper and AVS inputs, the hosted vault widget, and — for FloPayAutomaticPaymentButton — the fallback modal that opens when a saved-payment charge can't complete silently.

| theme | Aesthetic | |---|---| | 'classic' | Historic FloPay look (no bundle applied — #EDEDFF wrapper, indigo submit). | | 'modern-light' / 'modern-dark' | Clean & airy, Inter, soft shadows, FloPay-blue accents. | | 'bold-light' / 'bold-dark' | Saturated FloPay blue with gradient pill submit and heavy borders. | | 'glass-light' / 'glass-dark' | Translucent surfaces with backdrop blur over a blue gradient. |

<FloPayCheckout
  sessionId="sess_abc123"
  theme="bold-dark"
  onComplete={handleSuccess}
/>

<FloPayAutomaticPaymentButton
  sessionId="sess_abc123"
  theme="bold-dark" // applied to the button AND the fallback modal
  onSuccess={handleSuccess}
/>

appearance (Stripe-side overrides) and buttonsStyles (per-field wrapper overrides) still work — both win over the resolved bundle for the fields they touch, so you can layer customizations on top of a theme:

<FloPayCheckout
  theme="modern-light"
  buttonsStyles={{ submitButton: { backgroundColor: '#FF0099' } }} // wins
/>

Buttons Layout

Switch from the default form layout to stacked payment buttons with an expandable card form:

<FloPayCheckout
  sessionId="sess_abc123"
  layout="buttons"
  theme="bold-dark"
  onButtonClick={(method) => console.log('clicked:', method)}
  onComplete={handleSuccess}
/>

layout can be changed at runtime. Switching a mounted FloPayCheckout from the default layout to "buttons" keeps the resolved checkout session and its gateway capabilities, so the Credit / Debit Card choice remains available without recreating the session.

Deprecated: the legacy buttonsTheme prop ('default' / 'minimal' / 'rounded' / 'dark') still works but new code should use theme so the same value drives both the buttons-layout wrapper and the auto-payment fallback. See ButtonsLayoutStyles reference for the underlying override fields.

Button Hooks

Use onButtonClick to track button interactions, onDecline to track declines/cancellations, and onBeforeButtonClick to enrich any buttons-layout payment method before it continues:

<FloPayCheckout
  layout="buttons"
  createSession={{
    clientId: 'your-client-id',
    currency: 'EUR',
    items: [{ providerItemId: 'product-1', providerItemName: 'Widget', totalAmount: 29.99 }],
    account: { userId: 'user_1', email: '[email protected]' },
    successUrl: '/success',
    cancelUrl: '/cancel',
  }}
  onBeforeButtonClick={async ({ method, createSession }) => {
    if (method !== 'card') {
      return {
        tagsData: { sessionId: `checkout_${method}_clicked` },
      };
    }

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

    return {
      account: { email },
      tagsData: { sessionId: 'checkout_email_captured' },
    };
  }}
  onButtonClick={(method) => {
    window.dataLayer?.push({ event: 'checkout_button_click', payment_method: method });
  }}
  onDecline={(decline) => {
    window.dataLayer?.push({ event: 'checkout_decline', ...decline });
  }}
  onComplete={handleSuccess}
/>

onBeforeButtonClick runs for every payment button in layout="buttons". Branch on method if only some flows need extra work. For PayPal, Apple Pay, and Google Pay, keep the hook fast because it runs during the provider button handshake before the wallet sheet or PayPal window opens.

Inline Session Creation

Skip the backend API route — create the session directly in the component:

<FloPayCheckout
  layout="buttons"
  createSession={{
    clientId: 'your-client-id',
    currency: 'EUR',
    items: [{ providerItemId: 'product-1', providerItemName: 'Widget', totalAmount: 29.99 }],
    account: { userId: 'user_1', email: '[email protected]', firstName: 'Jane', lastName: 'Doe' },
    successUrl: '/success',
    cancelUrl: '/cancel',
  }}
  onComplete={(result) => window.location.href = '/success'}
/>

The component POSTs to the billing API, gets the full session back, and renders the form — zero backend code needed.

Authorisation-only checkout

Set captureMethod: 'manual' in the same createSession draft for an eligible item-only card checkout:

<FloPayCheckout
  createSession={{
    clientId: 'your-client-id',
    captureMethod: 'manual',
    currency: 'EUR',
    items: [{ code: 'order_123', totalAmount: 29.99 }],
    account: { userId: 'user_1', email: '[email protected]' },
    successUrl: '/success',
    cancelUrl: '/cancel',
  }}
  onComplete={(result) => {
    if (result.status === 'authorized') {
      saveAuthorisation({
        paymentId: result.paymentId,
        sessionId: result.sessionId,
        expiresAt: result.authorizationExpiresAt,
      });
    }
  }}
  onSessionCompleted={(successUrl) => router.push(successUrl)}
/>

The component still releases the buyer to the success experience, but its overlay says PAYMENT AUTHORISED. authorized means funds are held, not paid. Capture remains a merchant-authenticated REST operation and is never available to browser code. Manual capture is rejected for subscription carts. See the repository's pre-authorisation guide.

That create POST sends a stable Idempotency-Key header automatically whenever a secure RNG is available. FloPayCheckout resolves the key once per logical checkout, stores it with the inline-session cache in sessionStorage, and reuses it across transport retries, effect reruns, and component remounts. Concurrent mounts (StrictMode double-mount, Suspense remount, or navigation flicker) share both the session create and the auto-mode payment attempt, so one purchase cannot fan out into multiple creates or charges.

Logical checkout identity is based on the merchant, buyer, cart/pricing, and coupons. Checkout-mode changes, refreshed payment tokens, and analytics/UTM prop churn do not rotate the key while that purchase is active. A different cart or buyer gets a different key; an email-less buyer is separated by account.userId.

To control the key yourself — for example to keep it stable across your own server retries — pass idempotencyKey on createSession:

<FloPayCheckout
  createSession={{
    /* … */
    idempotencyKey: `checkout:${orderId}`, // one logical checkout; never reuse for a new purchase
  }}
/>

The key must be non-empty and at most 255 characters; an invalid value throws a FloPayError('validation_error') before any request. Omit it and the SDK generates a fresh cryptographically random key per logical create when a secure RNG is available; without one it omits the header, and a merchant-supplied idempotencyKey stays the way to remain idempotent. See the @flopay/js idempotency docs for the full contract.

Session-level currency is now required. The backend (#760) enforces @IsNotEmpty on the field; the SDK pre-validates and throws FloPayError({ type: 'validation_error', code: 'CurrencyRequired' }) before issuing the request when neither the session nor any item/subscription/product carries a currency. Pass currency at the top of createSession (preferred), or rely on the legacy fallback to the first item/subscription/product currency.

When you use onBeforeButtonClick with createSession, any returned InlineSessionPatch is merged into the draft session params before the selected buttons-layout flow continues. That lets you add tracking data or update account fields just in time without pre-creating a separate backend session.

Detached session creation (default)

createSession checkouts are created in two phases (TeamFloPay/backend#1099): a lightweight session shell that skips catalog validation and the buyer-identity advisory locks, then a background claim that attaches buyer identity, address, products and coupons.

FloPayCheckout renders from the shell, so the hosted card form mounts and becomes interactive without waiting for either the claim or Stripe.js — Stripe wallets, APMs and PayPal load in parallel with the claim and appear when it lands. The card widget's submit button stays gated for that window, because the billing API rejects a charge against an unclaimed session; if a buyer clicks during it, the widget cancels the click and the form asks them to press pay again. In practice the claim resolves while the card is still being filled in.

Nothing else changes for you: the same session, the same callbacks, the same results. Catalog and coupon errors still surface as a checkout load error, just from the claim rather than the create.

Opt out per session to restore the original single-request create:

<FloPayCheckout
  createSession={{
    /* … */
    deferDataAttachment: false, // one-shot create
  }}
/>

Detached creation applies only to checkoutMode: 'full' (the billing API rejects it for auto / confirm) and is skipped when tokenizedData is supplied. There is no fallback to the one-shot create — it requires a billing API that exposes PATCH /v1/checkouts/sessions/{id}/claim.

Advanced: Manual Provider Setup

Use SplitCardForm when you need to compose the provider and session yourself. Card checkout uses the backend-hosted vault widget; Stripe remains limited to wallets/APMs and saved-payment authentication. The session can supply the widget eagerly in session.vault, or explicitly advertise card in gateways.stripe.enabledPaymentMethods so the SDK recovers it on demand.

import { FloPayProvider, SplitCardForm } from '@flopay/react';
import { loadFloPay } from '@flopay/js';

const floPayPromise = loadFloPay('pk_test_...');

function CheckoutPage() {
  return (
    <FloPayProvider
      flopay={floPayPromise}
      options={{
        amount: 2999,
        currency: 'usd',
      }}
    >
      <SplitCardForm
        sessionId="session_uuid"
        nonce={sessionNonce}
        session={session}
        billingApiUrl="https://billing.example.com"
        email="[email protected]"
        userId="user_1"
        totalAmount={2999}     // cents
        currency="usd"
        enabledPaymentMethods={session.gateways?.stripe?.enabledPaymentMethods}
        onComplete={(result) => {
          if (result.status === 'succeeded') {
            window.location.href = '/success';
          }
        }}
        onError={(err) => console.error(err)}
        onFirstNameChange={(name) => setFirstName(name)}
        onLastNameChange={(name) => setLastName(name)}
      />
    </FloPayProvider>
  );
}

The resulting surfaces are:

  1. Wallet buttons and supported APM tiles from the session gateway capability.
  2. Direct or Stripe-hosted PayPal, selected from the session gateways.
  3. The hosted vault widget for sessions with embedded vault HTML or an explicit Stripe card capability.

Wallets, APMs, and PayPal create intents through POST /v1/checkouts/sessions/{id}/intents. The discriminated request separates provider, payment-method category/type/id, and intent kind. Direct-card intent requests are unsupported. Client-observed non-card failures use the nonce-protected session decline endpoint and contain no payment tokens, provider object IDs, card data, credentials, or PII.

For SDK 1.4.9+, an explicit Stripe card capability keeps card checkout visible even when the backend omits the vault block. In layout="buttons", POST /v1/checkouts/sessions/{id}/vault/capture is deferred until the buyer selects Card. An embedded block remains the preferred fast path and causes no recovery POST. Sessions that advertise neither an embedded block nor card do not call the recovery endpoint; their non-card methods remain usable, and if none remain, onError receives UnsupportedBackendVaultCapability.

AVS postcode validation

When AVS collection is enabled (enableAVS) and the postcode field is shown, SplitCardForm validates the postcode format against the live selected country before the hosted vault captures the card. It reuses @flopay/shared's country-aware postcode helpers (the same validator rules the billing API applies), so client and server agree.

  • Supported country + malformed postcode → the vault widget's submit is blocked and an inline, country-specific message shows the expected format (e.g. "Enter a valid ZIP Code (e.g. 12345 or 12345-6789)"). On the vault path the inline message appears once the field is blurred, since the disabled submit means it can't be reached by a submit attempt.
  • Supported country + empty postcode → the same submit gate applies, and once the field is blurred (or a submit is attempted) an inline required message shows (e.g. "ZIP Code is required") rather than the expected-format copy.
  • Country with no postal system (or a locale validator doesn't recognise) → the postcode field is shown but optional: empty and format checks are both skipped, so these buyers are never blocked.

On the vault card path the widget's submit button is gated synchronously on the client-side AVS check above. Because the client postcode rules mirror the backend validator ([email protected]) verbatim, a postcode the server's PATCH /v1/checkouts/sessions/{id}/account would reject is already blocked here — before the button ever enables — with no network round-trip in the critical path. The gate is never held pending an async request: the hosted widget silently swallows a click that lands while the button is disabled and offers no way to auto-resubmit on release, so an async gate would strand any buyer (or single-click preview) who clicked before it resolved — the checkout would write no attempt and time out (sdk#124).

When the buyer submits, SplitCardForm persists the billing snapshot best-effort in parallel with the widget's charge:

  • Backend rejects the address (a 4xx) — a residual, non-postcode drift, since the postcode is already client-validated — the specific message (e.g. "Postal/ZIP code is not valid for the selected country…") shows inline and fires onError with the parsed FloPayError, and the processing overlay clears so the buyer can correct and retry. The charge cannot be cancelled once submitted, so this surfaces for the buyer's next attempt rather than stopping the in-flight one.
  • Transient failure (5xx / network) → best-effort and buyer-silent: the backend listener falls back to the session's baseline address while the widget completes the charge.
  • Internal 10-second timeout → the same functional fallback applies, and successful widget completion still reaches onComplete. Telemetry records the privacy-safe lifecycle event operation.fallback with processing / account_snapshot / timeout dimensions instead of an alert-level technical error. Direct and blocking timeout paths remain errors.

Vault PCI card form

The card path renders a backend-served, self-contained hosted vault widget (VaultCardFields) (TeamFloPay/backend#823, Model A). The billing API may return it eagerly in session.vault, or advertise card under gateways.stripe.enabledPaymentMethods and let SDK 1.4.9+ request it lazily. It is fully server-driven — there is no consumer prop to toggle it. The widget owns the card fields, its own submit button, card tokenization, the PaymentIntent (created and confirmed server-side), 3DS, and the result, so no Stripe.js runs on the card path and PAN / CVC never enter the SDK runtime. Wallets / PayPal / APMs render exactly as before.

Because the widget owns the form, SplitCardForm exposes no SDK card-entry or card-submit controls. Host-collected AVS fields remain outside the PCI widget and gate its submit. The flow is:

session embeds session.vault or advertises gateways.stripe.enabledPaymentMethods: ['card']
  → SDK uses embedded HTML, or requests POST /vault/capture when Card is needed
  → concurrent renders/remounts share that active request
  → buyer pays inside the widget (tokenize → charge → 3DS, all backend-owned)
  → widget postMessages its outcome → SDK fires onComplete / onDecline / onError
  • Embedded session.vault.html is the preferred fast path during rollout and never triggers a recovery request.
  • Deferred capture uses POST /v1/checkouts/sessions/{id}/vault/capture, with a ten-second timeout, bounded transient-network retry, and one shared active request per base URL/session/nonce.
  • Returning customers with a card on file (session.providerPaymentMethodId) are charged by the backend's auto-checkout cascade.
  • 3DS is handled inside the widget — there is no client-side confirmCardPayment.

Processing feedback: after the hosted widget emits submitting, the SDK's full-checkout processing overlay remains visible continuously until the widget reports complete, decline, or error. Intermediate status polling and late card-field validation mutations cannot expose the completed form while a charge is still in flight. A synchronous card-validation rejection still returns the buyer to the form immediately, as does a 4xx account-snapshot validation failure.

Failure modes: onDecline / onError receive the mapped reason + message so consumers can react, and the SDK leaves an inline retry message after its terminal failure overlay clears.

Backend dependencies: the hosted widget must emit the flopay-vault postMessage outcome the SDK listens for (otherwise it falls back to its own success redirect), handle the 3DS step internally, and use the session account snapshot for AVS. The SDK collects and validates those address fields outside the PCI widget and persists them through the session account endpoint; the widget owns only sensitive card-data capture. Because the widget is injected same-window, every terminal outcome must be bound to the session id, and — to fully defend against same-window forgery — the backend should mint a per-session messageToken on the vault block and echo it in each postMessage; the SDK threads it through and rejects outcomes that omit or mismatch it (TeamFloPay/backend#823).

PayPal Handling

The SDK supports two PayPal paths, selected per-session based on what the billing API advertises under gateways.*:

  • Direct PayPal (preferred where available — works inside Facebook, Instagram, and other in-app browsers): when the session exposes gateways.paypal.publishableKey, the SDK renders PayPal via the official PayPal JS SDK using <DirectPayPalButton>. The PayPal client ID and environment ('sandbox'/'live') come directly from the backend.
  • Stripe-rendered PayPal: when only gateways.stripe is configured, PayPal renders through Stripe's ExpressCheckoutElement in its own Elements wrapper. This path can't render in Facebook/Instagram in-app browsers.

Renderer selection is mutually exclusive per session — direct PayPal takes priority over Stripe-rendered PayPal. Consumer props such as showPayPal continue to gate visibility on the client side.

PayPal-only sessions: When the backend advertises only gateways.paypal (no gateways.stripe), FloPayCheckout skips Stripe Elements entirely and renders <DirectPayPalButton> as the sole payment surface. Sessions that advertise no supported gateway at all throw a validation_error explaining the expected shape.

Flo-owned subscription continuations: gateways.paypal.providerObjectType selects the direct PayPal operation. 'order' loads the PayPal SDK with intent=capture and invokes createOrder, even when the checkout session mode is subscription. 'subscription' preserves the provider-managed intent=subscription / createSubscription flow. 'setup_token' invokes createVaultSetupToken; approval submits the returned vaultSetupToken to the nonce-bound session /process route for backend exchange and activation. An advertised 'order'/'setup_token' opts the intent request and tokenized process body into the paypal_vaulted payment-method channel; the intent response must match the advertised object type. When the field is absent, legacy sessions continue to derive Order versus Subscription from isSubscription and keep the released paypal requests byte-identical. See the full channel-selection and cutover contract.

Direct PayPal initialization recovery: PayPal's own cross-window bridge owns its 10-second postMessage init() acknowledgement deadline; FloPay does not change that upstream timeout. If the exact acknowledgement timeout is reported, or if Buttons.render() is still unsettled after 11 seconds, the SDK hides Direct PayPal, waits one second, and makes exactly one background render retry. The retry uses a fresh render generation, and callbacks or promise settlements from the superseded generation are ignored.

In a mixed checkout, card, wallets, APMs, and any other eligible methods remain interactive throughout recovery. If both Direct PayPal attempts fail, PayPal stays hidden for that component configuration; the SDK emits one sanitized console diagnostic and calls neither onError nor onDecline. Deterministic configuration failures and isEligible() === false are not automatically retried.

In a PayPal-only checkout, FloPayCheckout shows an accessible retrying status instead of a blank surface. After automatic recovery is exhausted, it shows safe generic unavailable copy and a Retry PayPal button. onError fires once with a FloPayError whose type is api_error and whose stable code is paypal_init_timeout; onDecline does not fire and the raw provider message is never rendered. Manual retry reuses the current checkout session and performs one fresh render attempt without adding another automatic retry.

The SDK exposes the relevant pieces in three ways:

  • SplitCardForm / FloPayCheckout: pick the renderer automatically based on gateways.*. No additional configuration needed.

  • FloPayProvider (manual composition): accepts an optional paypalFlopay prop used to drive Stripe's PayPal redirect leg. Load a second FloPay instance yourself only if you need to render PayPal manually. usePayPalFloPay() exposes it to descendants.

  • DirectPayPalButton (standalone): can be rendered outside SplitCardForm when you only need the PayPal button. Pass clientId, currency, environment, isSubscription, and the backend-advertised providerObjectType when present. isSubscription remains the compatibility fallback when the field is absent.

Using Hooks

import { useFloPay, useCheckout } from '@flopay/react';

function PaymentStatus() {
  const flopay = useFloPay();       // FloPay | null
  const checkout = useCheckout();    // { session, loading, error, claimPending }

  if (!flopay) return <div>Loading SDK...</div>;
  // ...
}

API Reference

Components

| Component | Description | |-----------|-------------| | FloPayProvider | Context provider. Accepts flopay (instance or promise), optional paypalFlopay, options?, and children. | | FloPayCheckout | Recommended self-contained session checkout. Resolves gateways, mounts hosted-vault cards, and preserves wallets/APMs/PayPal/saved-payment flows. | | FloPayCardSetup | Browser-only no-charge card verification for a merchant-server-created setup session. Requires sessionId + nonce; reports ready, verified completion, decline, validation, retryable technical error, and cancellation outcomes. | | SplitCardForm | Advanced checkout surface combining hosted-vault cards with wallets, APMs, and PayPal. Supports ref for imperative next-action handling. | | VaultCardFields | Hosted vault PCI card fields. Used internally by SplitCardForm on the vault path; consumes a CardCaptureAdapter from useFloPay().cardCapture(). | | DirectPayPalButton | Standalone direct PayPal Order, provider-managed Subscription, or setup-token button using the session-scoped intent contract. |

Hooks

| Hook | Returns | Description | |------|---------|-------------| | useFloPay() | FloPay \| null | Current FloPay instance from context. null while loading. | | useCheckout() | CheckoutState | { session, loading, error, claimPending } from CheckoutContext. claimPending remains true while a detached shell is not safe to charge. |

FloPayProviderProps

| Prop | Type | Description | |------|------|-------------| | flopay | Promise<FloPay> \| FloPay | SDK instance or promise from loadFloPay() | | paypalFlopay | Promise<FloPay> \| FloPay \| null | Optional Stripe instance used for the Stripe-rendered PayPal redirect leg. | | options.billingApiUrl | string? | Billing API base URL exposed to child components. | | onInstrument | (event: FloInstrumentEvent) => void | Versioned, privacy-safe checkout funnel and phased error feed for merchant analytics. |

FloPayCardSetupProps

| Prop | Type | Description | |------|------|-------------| | sessionId | string | Opaque id returned by the merchant-authenticated setup-session endpoint. | | nonce | string | Required session-bound read/capture token. | | billingApiUrl | string? | Billing API base URL; defaults through normal FloPay environment resolution. | | telemetry | boolean? | Privacy-safe operational telemetry; set false to opt out. | | theme | ThemeId \| VaultCardThemeColors? | High-level theme id (same values as FloPayCheckout, resolved through the same bundle) or raw hosted-widget colors. Defaults to 'modern-light'; 'classic' keeps the historic FloPay look. | | containerStyle | React.CSSProperties? | Inline styles for the hosted-widget container. | | loading | React.ReactNode? | Content shown while the session and hosted widget load. | | onReady | () => void | Hosted widget is mounted and accepting input. | | onComplete | (event: FloPayCardSetupCompleteEvent) => void | Verified usable card. Includes sessionId and optional display-only paymentMethod; never amount, charge, or payment-success data. | | onDecline | (event: FloPayCardSetupDeclineEvent) => void | Terminal card-verification decline. | | onValidation | (message: string \| null) => void | Live hosted-field validation; not a technical error. | | onError | (error: FloPayCardSetupError) => void | Validation/technical failure with stable code and explicit retryable. | | onCancel | (event: FloPayCardSetupCancelEvent) => void | Fires once when the component unmounts before a terminal outcome. |

Privacy-safe operational telemetry

FloPayCheckout emits the same closed Flo-owned lifecycle/error/performance contract as @flopay/js, including React render/interactivity and provider readiness milestones. Collection is best-effort; merchant callbacks remain available with their existing arguments and invocation order. Synchronous throws and rejected callback promises are contained and logged only to the merchant console; they are not uploaded as CALLBACK_FAILED. Set the single telemetry={false} prop to opt out:

<FloPayCheckout sessionId={sessionId} nonce={nonce} telemetry={false} />

No endpoint, custom tag, user context, message, stack, or metadata can be configured. See docs/TELEMETRY.md for the complete privacy and retention contract.

If the React host has its own Sentry client, use the re-exported pure filter as the host's beforeSend callback:

import * as Sentry from '@sentry/browser';
import { dropThirdPartyOnlyError } from '@flopay/react';

Sentry.init({ beforeSend: dropThirdPartyOnlyError });

No Sentry package or configuration is bundled by FloPay. The predicate drops only complete exception stacks with no app or @flopay/* frame and at least one recognised browser-extension URL or Stripe dahlia/stripe.js path/module marker. Anonymous frames may accompany that marker. Mixed, unrelated, incomplete, and anonymous-only errors pass through unchanged. The in_app flag is ignored because host rewriting may set it to true on third-party frames.

Inline session creation classifies bounded failures without inspecting raw errors: network/CORS/timeouts emit transport_error, 5xx responses emit server_error, and malformed response discriminants emit invalid_response. Stripe and PayPal SDK load/runtime failures emit provider_runtime. Inline 4xx validation rejections and ordinary payment declines remain non-error validation_rejected / payment_declined outcomes and never carry failureCategory.

The same classification applies to React-owned saved-payment processing in auto and confirm modes. An ordinary saved-payment 400 decline, a structured-field 400, or a 422 still falls back to the usable full checkout and emits operation.fallback, but reports only the matching payment_declined or validation_rejected outcome. Genuine server, transport, malformed-response, and provider-runtime failures remain categorized technical errors. This does not change fallback rendering or merchant callback behavior, and no response body, provider payload, session value, payment data, or raw error message is added to telemetry.

SplitCardFormProps

Key payment-surface props include:

| Prop | Type | Description | |------|------|-------------| | sessionId | string | Checkout session UUID. | | nonce | string | Session-bound token forwarded on intent, decline, account, and process calls. | | session | CheckoutSession? | Session capability data, including the optional hosted vault fast path and gateways. | | billingApiUrl | string | Billing API base URL. | | showStripe | boolean | Show Stripe-backed wallets/APMs and the hosted card path when the session embeds vault or advertises card. | | showPayPal | boolean | Show PayPal. Renderer chosen by gateways.paypal presence (DirectPayPal JS SDK when present; Stripe-rendered PayPal otherwise). Default: true. | | enabledPaymentMethods | string[]? | Per-session list of Stripe method type identifiers (card, apple_pay, google_pay, cashapp, klarna, link, amazon_pay, sepa_debit, affirm, ideal, …). Normally threaded automatically from gateways.stripe.enabledPaymentMethods by FloPayCheckout. The SDK partitions non-card methods between ExpressCheckoutElement and PaymentElement; card advertises the hosted-vault path and is never rendered by Stripe. | | showApplePay | boolean | Deprecated. Apple Pay availability is dashboard-controlled at Stripe and surfaces through enabledPaymentMethods. Emits a one-time console.warn when supplied alongside enabledPaymentMethods and is otherwise ignored. Removed in 2.0. | | showGooglePay | boolean | Deprecated. See showApplePay. | | directPaypal | { clientId: string; environment?: GatewayEnvironment }? | Deprecated input on FloPayCheckout — auto-resolved from gateways.paypal on the session response. Still accepted on SplitCardForm for advanced consumers wiring providers manually. | | totalAmount | number | Amount in cents for PayPal / wallet config | | currency | string | Currency code for PayPal / wallet config (default: 'usd') | | onFirstNameChange | (value: string) => void | First name change callback | | onLastNameChange | (value: string) => void | Last name change callback | | onComplete / onDecline / onError | callbacks | Observable checkout outcomes. |

Deferred-vault rollout boundary

SDK 1.4.9 is the compatibility boundary for omitting embedded capture credentials. Keep the backend's current link session → capture → complete idempotency → 201 ordering for requests from older or unknown SDK versions, and until the deployed @flopay/react population has crossed the adoption threshold chosen by the backend rollout. The SDK advertises its version in x-flo-sdk-version on session create/read.

After that threshold, the backend may omit capture credentials from create/read/idempotent-replay responses for requests advertising SDK >=1.4.9, provided the session explicitly includes card in gateways.stripe.enabledPaymentMethods and the nonce-protected POST /v1/checkouts/sessions/{id}/vault/capture route remains available. Backend rollout verification should confirm that create/read/replay no longer invoke PCIVault and that capture issuance occurs only through the deferred endpoint when a buyer enters the card path.

Types

| Type | Description | |------|-------------| | FloPayProviderProps | Props for FloPayProvider | | FloPayCardSetupProps | Props for FloPayCardSetup | | FloPayCardSetupCompleteEvent | Verified setup completion with optional display-only paymentMethod | | FloPayCardSetupDeclineEvent | Terminal card-verification decline | | FloPayCardSetupCancelEvent | Unmount-before-terminal cancellation | | FloPayCardSetupError | Structured setup failure with stable code and retryable classification | | SplitCardFormProps | Props for SplitCardForm | | CheckoutState | { session, loading, error, claimPending } | | FloInstrumentEvent | Versioned merchant instrument union with seven lifecycle names, four checkout_error phases, and an optional gateway. |