DevelopersTools

Browser (@fianto/js)

Open Fianto checkout from the browser with openCheckout, redirectToCheckout or the <fianto-button> element, and read its four result statuses.

4 min read

@fianto/js runs in the browser. It opens the hosted checkout page for a session your server created, in a popup or as a full-page redirect, and reports how the popup ended. It never holds your app secret. <fianto-button> is the same thing as a ready-made "Pay with Fianto" button for pages without a build step.

Before you start

  • A checkout route on your server that answers { id, url }: Checkout() in Next.js, checkout() in Express or Hono, or createCheckoutHandler.
  • The page that opens checkout is served from the same origin as the session's success_url. The checkout page posts its result only to that origin.

Steps

Install the package

pnpm add @fianto/js

Without a bundler, use the script build instead (step 5).

Open checkout from a click

Call openCheckout synchronously inside the click handler, with no await or setTimeout before it. It opens the popup first and only then waits for your route, so the browser's popup blocker sees a direct response to the click.

import { fetchCheckoutSession, openCheckout } from '@fianto/js';

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.',
};

document.querySelector('#pay')?.addEventListener('click', () => {
  openCheckout({
    session: () => fetchCheckoutSession('/api/checkout', { body: { item: 'beans' } }),
    fallback: 'redirect', // the default: if the popup is blocked, send the whole page to checkout
    popup: { width: 480, height: 720 }, // the default size
  }).then(
    (result) => {
      document.querySelector('#status')!.textContent = MESSAGES[result.status] ?? '';
    },
    (error) => console.error(error),
  );
});

session is a { id, url } you already have, or a function that returns a promise of one. Use fetchCheckoutSession(endpoint, { body }) for that function: it posts body as JSON with credentials: 'same-origin' and rejects with a CheckoutSessionError (code, status, retryAfter) when your route refuses, for example payment_in_progress. It rejects with network_error when the route cannot be reached, and session_request_failed when it answers an error without a { error: { code } } body.

Clicking again while a checkout is open should bring its popup to the front rather than start a second one: focusCheckout() does that and returns false when no popup is open. A second openCheckout() call takes over the popup, and the first call resolves closed with reason superseded.

Act on the result

statusMeaning
succeededThe payer's transaction was confirmed on the checkout page. Not proof that the order is paid.
canceledThe payer canceled on the checkout page.
expiredThe session expired.
closedUnknown. No result reached your page. reason says why.
reason (with closed)Meaning
closed_by_payerThe popup was closed before checkout opened in it, or more than about 1.5 s after it opened checkout.
unreachableThe popup was cut off within about 1.5 s of opening checkout, almost always by your page's Cross-Origin-Opener-Policy: same-origin. Checkout may still be open.
supersededA newer openCheckout() call took over the popup.
returned_from_redirectThe page was sent to checkout and the payer came back through the browser's back/forward cache.

Fulfil from Fianto, not from the browser

succeeded is what the payer's browser saw. Mark the order paid only from a verified order.paid webhook or a server-side retrieve of the order. Neither the result nor a visit to success_url proves payment.

closed means unknown

closed tells you nothing about the payment: a payment may still be confirming. Tell the payer to check their order status before paying again.

Handle popup blockers and full-page checkout

If the browser blocks the popup, fallback: 'redirect' sends the whole page to checkout and the promise normally never settles. With fallback: 'none', openCheckout rejects with a PopupBlockedError instead. To skip the popup entirely, use redirectToCheckout:

import { fetchCheckoutSession, redirectToCheckout } from '@fianto/js';

document.querySelector('#pay')?.addEventListener('click', () => {
  // Sends the whole page to checkout. Resolves `closed` only if the payer comes back
  // through the back/forward cache.
  void redirectToCheckout(() => fetchCheckoutSession('/api/checkout', { body: { item: 'beans' } }));
});

Or drop in <fianto-button>

With a bundler, import '@fianto/js/button' registers the element. Without one, serve the file dist/fianto-button.global.iife.js from the installed @fianto/js on your own site, at the path your <script> tag uses. That build also sets window.Fianto to openCheckout, redirectToCheckout, fetchCheckoutSession and focusCheckout.

<script src="/fianto-button.js"></script>

<fianto-button session-endpoint="/api/checkout" data-item="beans" label="buy"></fianto-button>
<p id="status" role="status"></p>

<script>
  var button = document.querySelector('fianto-button');
  button.addEventListener('fianto:result', function (event) {
    // event.detail is the openCheckout result: { status, session_id, reason? }
    document.getElementById('status').textContent = event.detail.status;
  });
  button.addEventListener('fianto:error', function (event) {
    console.error(event.detail.code, event.detail.message); // message is for you, not the payer
  });
</script>

Loading the script from a CDN

If you load fianto-button.global.iife.js from a CDN instead of your own site, pin an exact version in the URL and add an integrity (SRI) hash with crossorigin="anonymous", so the browser refuses any other file.

On click, the button posts its data-* attributes as the JSON body to session-endpoint, so data-item="beans" arrives as { "item": "beans" }. Setting the element's session property (a { id, url } or a function returning a promise of one) overrides session-endpoint. With neither, the click fires fianto:error with code no_session_source. Both events bubble and cross shadow roots, so you can listen on any ancestor.

AttributeValuesDefault
session-endpointYour checkout route's URL—
themebrand, dark, light, outline, autobrand
labelplain (logo only), pay, buy, checkout, subscribe, donatepay
shaperect, rounded, pillrounded
sizestatic, fill (full width)static
localeen, vithe browser's language if it is one of these, else en
fallbackredirect, noneredirect
disabledboolean—

An unknown value falls back to the default. Size it with the CSS variables --fianto-button-height (40 to 55px, default 44px), --fianto-button-radius, --fianto-button-width (default auto) and --fianto-button-focus-ring, set on the element.

When your route refuses, the button shows the payer a short line chosen by the error's code, never your route's message. After a closed result with reason unreachable, it shows "Lost track of the checkout window. Check your order status before trying again." and ignores clicks for 6 s. A click while a checkout is open brings its popup to the front.

Let the popup talk to your page

If your page sends a Cross-Origin-Opener-Policy header, set it to same-origin-allow-popups, or send none. With same-origin, the popup is cut off as soon as it opens checkout: the result resolves closed with reason unreachable while the payer may still be paying, and the console shows a warning naming this fix.

Check it worked

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

Troubleshooting

You seeWhyFix
The popup is blocked every timeCheckout was opened after an await or a setTimeout, not directly in the click.Call openCheckout synchronously in the click handler.
closed with reason unreachable while the popup still shows checkoutYour page sends Cross-Origin-Opener-Policy: same-origin.Send same-origin-allow-popups on the page that opens checkout.
No result arrives; the promise resolves closed when the popup closesThe page is not on the origin of the session's success_url.Serve the page from the same origin as success_url.
CheckoutSessionError with code forbidden_origin (403)Your checkout route refused a cross-site call.Call it from your own site, or set allowedOrigins on the route.
CheckoutSessionError with code internal_error (500)Fianto refused for a reason the route does not pass to the browser.Read the error the route's onError logged on your server.
CheckoutSessionError with code payment_in_progress (409)A payment for this order is already under way.Wait for it to finish and check the order status before trying again.
fianto:error with code no_session_sourceThe button has neither session-endpoint nor a session property.Set one of them.

See also

Was this page helpful? Tell us

On this page