Examples
FloPayCheckout
Complete checkout page example using the all-in-one FloPayCheckout component.
FloPayCheckout Example
A full checkout page with order summary, discount timer, and FloPayCheckout handling all payment logic.
Complete Page
'use client';
import type { NormalizedCheckoutSession, PaymentResult } from '@flopay/shared';
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
import { buildCheckoutDisplayData, configureFlopay, resolveBillingApiUrl } from '@flopay/shared';
import { FloPayCheckout } from '@flopay/react';
import { PaymentAPI } from '@flopay/js';
import { useSearchParams } from 'next/navigation';
// Configure once at module load
configureFlopay({ environment: 'production' });
function CheckoutContent() {
const searchParams = useSearchParams();
const sessionId = searchParams.get('id') ?? '';
const modeParam = searchParams.get('mode') as 'full' | 'auto' | 'confirm' | null;
const [session, setSession] = useState<NormalizedCheckoutSession | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [countdown, setCountdown] = useState(600);
const api = useMemo(() => new PaymentAPI(resolveBillingApiUrl()), []);
useEffect(() => {
if (!sessionId) return;
let cancelled = false;
(async () => {
try {
const unified = await api.getUnifiedCheckoutSession(sessionId);
if (!cancelled) {
setSession(unified);
if (unified.data.session?.status === 'complete')
window.location.href = '/success';
}
} catch (err) {
if (!cancelled)
setError(err instanceof Error ? err.message : 'Failed to load session');
} finally {
if (!cancelled) setIsLoading(false);
}
})();
return () => { cancelled = true; };
}, [sessionId, api]);
// Countdown timer
useEffect(() => {
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) { clearInterval(timer); return 0; }
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, []);
const handleComplete = useCallback((result: PaymentResult) => {
if (result.status === 'succeeded') window.location.href = '/success';
}, []);
const formatCountdown = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
if (!sessionId) return <p>No session ID. Browse products first.</p>;
if (isLoading) return <p>Loading...</p>;
if (!session?.data.session) return <p>Checkout Error: {error}</p>;
const cs = session.data.session;
const displayData = buildCheckoutDisplayData(cs);
const { items, currency, total, totalSave, discountPercent } = displayData;
const fmt = (n: number) =>
new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n);
return (
<div style={{
background: 'white', maxWidth: 526, margin: '0 auto',
borderRadius: '16px', boxShadow: '0 0 34px rgba(43, 51, 67, 0.12)',
overflow: 'hidden',
}}>
{/* Title */}
<div style={{ textAlign: 'center', padding: '1rem 1.5rem 0.25rem' }}>
<p style={{ fontSize: '1.5rem', fontWeight: 600 }}>Safe & Secure Checkout</p>
</div>
<div style={{ padding: '0.25rem 1rem 1.5rem', display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
{/* Discount timer */}
{countdown > 0 && totalSave > 0 && (
<div style={{
background: '#fef2f2', borderRadius: '8px', padding: '0.5rem',
textAlign: 'center', color: '#991b1b',
}}>
Your Discount Reserved for <strong>{formatCountdown(countdown)}</strong>
</div>
)}
{/* Error */}
{error && (
<div style={{ background: '#FAECE9', borderRadius: '8px', padding: '0.5rem 0.75rem', color: '#C72B23' }}>
Error: {error}
</div>
)}
{/* Order summary */}
<div>
{items.map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', padding: '0 0.75rem', marginBottom: '0.5rem' }}>
<span>{item.name.toUpperCase()}</span>
<span style={{ color: '#A8A9AD' }}>{fmt(item.originalPrice)}</span>
</div>
))}
{totalSave > 0 && (
<div style={{ background: '#EFF9F0', borderRadius: '8px', padding: '0.25rem 0.5rem', display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
<span style={{ color: '#7DAD3A', fontSize: '0.8rem', fontWeight: 500 }}>{discountPercent}% DISCOUNT APPLIED</span>
<span style={{ color: '#7DAD3A' }}>-{fmt(totalSave)}</span>
</div>
)}
<div style={{ borderTop: '1px solid #A8A9AD', margin: '0.5rem 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<p style={{ fontSize: '1.5rem', fontWeight: 600 }}>Total Due Today:</p>
<p style={{ fontSize: '1.5rem', fontWeight: 600 }}>{fmt(total)}</p>
</div>
</div>
{/* FloPayCheckout handles everything */}
<FloPayCheckout
sessionId={sessionId}
checkoutMode={modeParam ?? undefined}
onComplete={handleComplete}
onError={(err) => setError(err.message)}
/>
</div>
</div>
);
}
export default function CheckoutPage() {
return (
<Suspense fallback={<p>Loading...</p>}>
<CheckoutContent />
</Suspense>
);
}With Buttons Layout
<FloPayCheckout
sessionId={sessionId}
layout="buttons"
buttonsTheme="rounded"
onComplete={handleComplete}
onError={(err) => setError(err.message)}
/>With Confirm Mode
<FloPayCheckout
sessionId={sessionId}
checkoutMode="confirm"
renderConfirmButton={({ onConfirm, isProcessing }) => (
<button
onClick={onConfirm}
disabled={isProcessing}
style={{
width: '100%', padding: '1rem',
backgroundColor: '#4A49FF', color: 'white',
border: 'none', borderRadius: '8px',
fontWeight: 600, cursor: isProcessing ? 'not-allowed' : 'pointer',
}}
>
{isProcessing ? 'Processing...' : 'Confirm Purchase'}
</button>
)}
onComplete={handleComplete}
/>Key Points
configureFlopay()is called once at module load — not per renderbuildCheckoutDisplayData()formats session data for display (handles discounts, trial naming, item hiding)FloPayCheckoutautomatically readsemail,userId,amount, andcurrencyfrom the session- Error suppression: pass
error={() => <></>}to handle errors entirely viaonError - The component handles 3DS, PayPal redirects, and wallet payments internally