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.
'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
FloPayCheckoutmodal on the same page using the sametheme, 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_foundon cross-gateway saved-PM upsells) - builds the correct
tokenizedDatashape 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>