FloPayFloPay
Examples

Automatic Payment Buttons

React upsell example using FloPayAutomaticPaymentButton to keep purchases on the current page.

Automatic Payment Buttons Example

This example shows a same-page upsell on a success screen. The user keeps their original purchase details in view, clicks a single saved-payment button, and successful upsells are appended back into the page state.

app/success/UpsellSection.tsx
'use client';

import { useMemo, useState } from 'react';
import { FloPayAutomaticPaymentButton } from '@flopay/react';

type PurchaseEntry = {
  name: string;
  amount: number;
  currency: string;
};

const upsellProduct = {
  code: 'upsell_ai_pack',
  name: 'AI Supercharger Pack',
  totalAmount: 249,
  quantity: 1,
};
const upsellCurrency = 'USD';

function formatAmount(amount: number, currency: string) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
  }).format(amount);
}

export default function UpsellSection() {
  const [upsells, setUpsells] = useState<PurchaseEntry[]>([]);

  const buttonLabel = useMemo(() => {
    const amount = upsellProduct.totalAmount;
    return `Add ${upsellProduct.name} for ${formatAmount(amount, upsellCurrency)}`;
  }, []);

  return (
    <section style={{ display: 'grid', gap: '1rem' }}>
      <div>
        <h2>Original Purchase</h2>
        <p>Core Plan - {formatAmount(49, 'USD')}</p>
      </div>

      <div style={{ display: 'grid', gap: '0.5rem' }}>
        {upsells.map((item, index) => (
          <p key={`${item.name}-${index}`}>
            Upsell purchase {index + 1}: {item.name} - {formatAmount(item.amount, item.currency)}
          </p>
        ))}
      </div>

      <FloPayAutomaticPaymentButton
        clientId="client_123"
        currency={upsellCurrency}
        account={{
          userId: 'user_123',
          email: 'customer@example.com',
        }}
        products={[upsellProduct]}
        successUrl={`${window.location.origin}/success`}
        cancelUrl={`${window.location.origin}/success`}
        theme="bold-dark"
        onSuccess={() => {
          setUpsells((current) => [
            ...current,
            {
              name: upsellProduct.name,
              amount: upsellProduct.totalAmount,
              currency: upsellCurrency,
            },
          ]);
        }}
        onDecline={(decline) => {
          console.log('payment declined', decline.code);
        }}
        onError={(error) => {
          console.error(error.message);
        }}
      >
        {buttonLabel}
      </FloPayAutomaticPaymentButton>
    </section>
  );
}

Why This Pattern Works

  • the page stays on the original success route instead of redirecting to a separate checkout screen
  • the button gets FloPay's shared processing, success, and decline modal states automatically
  • if authentication is required, the SDK opens the fallback FloPayCheckout modal on the same page using the same theme, so the upsell button and its recovery modal stay visually consistent
  • successful upsells can update your local React state immediately

Backend Picks The Saved Payment Method

FloPayAutomaticPaymentButton no longer needs paymentMethodId or checkoutMethod. The backend's auto-checkout branch now:

  • resolves the customer's latest vaulted payment method via userPaymentMethodRepository.getLatestByUserId(userUuid)
  • rebinds the session's gateway when the latest payment method lives on a different provider than routing originally picked (fixes payment_method_not_found on cross-gateway saved-PM upsells)
  • builds the correct tokenizedData shape per provider ({ id: vaultToken } for Stripe, { userPaymentMethodId: uuid } for PayPal)

Pass only the customer identity (account.userId) and the upsell line items; the backend takes care of the rest.

Reusing A Session Created On Your Backend

If your backend creates the upsell session first, switch the button to sessionId mode. Forward the session's nonce alongside it — your backend returns it on the create response, and the SDK needs it to authenticate the session read and /process (omit it and post-#640 backends return 401 "Missing checkout session token."). The same theme still propagates to the fallback modal if authentication is required:

<FloPayAutomaticPaymentButton
  sessionId={upsellSessionId}
  nonce={upsellSessionNonce}
  theme="bold-dark"
  onSuccess={() => {
    setUpsells((current) => [
      ...current,
      {
        name: 'AI Supercharger Pack',
        amount: 80,
        currency: 'USD',
      },
    ]);
  }}
>
  Purchase Item
</FloPayAutomaticPaymentButton>

On this page