DevelopersTools
Browser (@fianto/js)
Open Fianto checkout from the browser with openCheckout, redirectToCheckout or the <fianto-button> element, and read its four result statuses.
@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
Steps
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
status | Meaning |
|---|---|
succeeded | The payer's transaction was confirmed on the checkout page. Not proof that the order is paid. |
canceled | The payer canceled on the checkout page. |
expired | The session expired. |
closed | Unknown. No result reached your page. reason says why. |
reason (with closed) | Meaning |
|---|---|
closed_by_payer | The popup was closed before checkout opened in it, or more than about 1.5 s after it opened checkout. |
unreachable | The 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. |
superseded | A newer openCheckout() call took over the popup. |
returned_from_redirect | The 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.
| Attribute | Values | Default |
|---|---|---|
session-endpoint | Your checkout route's URL | — |
theme | brand, dark, light, outline, auto | brand |
label | plain (logo only), pay, buy, checkout, subscribe, donate | pay |
shape | rect, rounded, pill | rounded |
size | static, fill (full width) | static |
locale | en, vi | the browser's language if it is one of these, else en |
fallback | redirect, none | redirect |
disabled | boolean | — |
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 see | Why | Fix |
|---|---|---|
| The popup is blocked every time | Checkout 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 checkout | Your 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 closes | The 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_source | The button has neither session-endpoint nor a session property. | Set one of them. |
See also
Was this page helpful? Tell us