DevelopersTools

React

Open Fianto checkout from React with the FiantoButton component or the useCheckout hook, in client components on React 19.

3 min read

@fianto/react wraps @fianto/js for React: FiantoButton is the branded pay button, and useCheckout tracks one checkout in React state for a button you design yourself. There is no provider and no key to configure: your server creates the session.

Before you start

  • React 19 or later (the peer dependency).
  • A checkout route on your server that answers { id, url }, such as Checkout() from @fianto/nextjs.
  • The page with the button is served from the same origin as the session's success_url.

Steps

Install the package

pnpm add @fianto/react

It installs @fianto/js with it, and re-exports fetchCheckoutSession and the error classes, so you import everything from @fianto/react.

Render FiantoButton in a client component

Every file the package ships starts with 'use client', so you can import it from a Server Component module. The component that renders the button and handles its callbacks must be a client component itself.

// app/checkout-button.tsx
'use client';

import { FiantoButton, fetchCheckoutSession } from '@fianto/react';
import { useState } from 'react';

const MESSAGES: Record<string, string> = {
  succeeded: 'Thanks! We are confirming your payment.',
  canceled: 'Checkout canceled.',
  expired: 'That checkout link expired. Check your order status before trying again.',
  closed: 'We could not tell what happened. Check your order status before paying again.',
};

export function CheckoutButton({ orderId }: { orderId: string }) {
  const [message, setMessage] = useState<string | null>(null);
  return (
    <div>
      <FiantoButton
        session={() => fetchCheckoutSession('/api/checkout', { body: { orderId } })}
        theme="brand"
        label="pay"
        onResult={(result) => setMessage(MESSAGES[result.status] ?? null)}
        onError={(error) => console.error(error)} // the route's message is for you
      />
      {message ? <p role="status">{message}</p> : null}
    </div>
  );
}

The button renders plain React markup, so the server and the client render the same DOM. When your route refuses, it shows the payer a short line chosen by the error's code and passes the full error to onError. A click while a checkout is open brings its popup to the front.

PropTypeDefault
session{ id, url } or () => Promise<{ id, url }> (required)—
theme'brand' | 'dark' | 'light' | 'outline' | 'auto''brand'
label'plain' | 'pay' | 'buy' | 'checkout' | 'subscribe' | 'donate''pay'
shape'rect' | 'rounded' | 'pill''rounded'
size'static' | 'fill''static'
locale'en' | 'vi'en on the server, then the browser's language after mount
fallback'redirect' | 'none''redirect'
loading, disabledbooleanfalse
onResult(result: CheckoutResult) => void—
onError(error: Error) => void—
className, styleas on any element—

ref reaches the underlying <button>. Pass locale when the server must render the same language the visitor sees; without it, a non-English visitor sees English until the component mounts.

Or build your own button with useCheckout

// app/pay-link.tsx
'use client';

import { fetchCheckoutSession, useCheckout } from '@fianto/react';

export function PayLink({ orderId }: { orderId: string }) {
  const { open, focus, isOpen, result, error } = useCheckout({
    session: () => fetchCheckoutSession('/api/checkout', { body: { orderId } }),
  });

  return (
    <div>
      {/* Call open() directly in the click handler, never after an await. */}
      <button onClick={() => (isOpen ? focus() : void open())} aria-busy={isOpen}>
        {isOpen ? 'Checkout open…' : 'Pay'}
      </button>
      {result ? <p role="status">Result: {result.status}</p> : null}
      {error ? <p role="alert">Checkout could not be started. Please try again.</p> : null}
    </div>
  );
}

useCheckout({ session, fallback? }) returns:

FieldType
openReturns a promise of the result, or of undefined. Never throws: on failure it sets error and resolves undefined.
focus() => boolean. Brings the open popup to the front; false when there is none.
status'idle' | 'open' | 'done' | 'error'
resultThe last CheckoutResult, or null
errorThe last error, or null. A CheckoutSessionError with a code when your route refused.
isOpenstatus === 'open'

Only the latest open() drives the state. Calling it again takes over the popup, and the earlier checkout resolves closed with reason superseded.

Read the result safely

result.status is succeeded, canceled, expired or closed, the same as openCheckout, with a reason on closed.

Fulfil from Fianto, not from the button

succeeded means the payer's transaction was confirmed on the checkout page, not that the order is paid. Mark it paid only from a verified order.paid webhook or a server-side retrieve.

closed means unknown

closed tells you nothing about the payment: one may still be confirming. With reason unreachable, checkout may still be open. Ask the payer to check their order status first.

Check it worked

Click the button. A popup opens Fianto's checkout page for the session your route created. Close the popup after the page has loaded: onResult receives closed with reason closed_by_payer.

Troubleshooting

You seeWhyFix
The popup is blockedopen() ran after an await, a setTimeout or inside a useEffect.Call open() directly in the click handler.
closed with reason unreachable right awayThe page sends Cross-Origin-Opener-Policy: same-origin.Send same-origin-allow-popups on that page.
Next.js errors when a Server Component renders FiantoButton or calls useCheckoutsession, onResult and onError are functions, and hooks run only in client components.Move that code into a file that starts with 'use client'.
error.code is internal_errorFianto refused for a reason your route does not pass to the browser.Read what the route's onError logged on your server.
The button shows English first, then another languageNo locale prop: the server renders en and the browser language applies after mount.Pass locale when the server knows the language.

See also

Was this page helpful? Tell us

On this page