Skip to content

Accept payments on the web

@usestack/react gives you StackProvider, usePaymentSheet and the Pay with Stack button. The sheet opens as a modal over your page. It renders in a Shadow DOM, so your CSS doesn’t change it and its CSS doesn’t leak into your page.

Terminal window
npm install @usestack/react

React 18 or 19.

main.tsx
import { StackProvider } from '@usestack/react';
import { createRoot } from 'react-dom/client';
import { Checkout } from './Checkout';
createRoot(document.getElementById('root')!).render(
// The publishable key is safe in the browser.
<StackProvider publishableKey="pk_test_...">
<Checkout />
</StackProvider>,
);
Checkout.tsx
import { PayWithStackButton, usePaymentSheet } from '@usestack/react';
import { useState } from 'react';
export function Checkout() {
const { presentPaymentSheet, loading } = usePaymentSheet();
const [message, setMessage] = useState<string | null>(null);
const pay = async () => {
// 1. Your server creates the payment and returns its client secret.
const res = await fetch('/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: [{ id: 'jollof', qty: 1 }] }),
});
const { orderId, clientSecret } = await res.json();
// 2. The Stack sheet signs the customer in and takes the payment. It never throws.
const result = await presentPaymentSheet({ clientSecret });
// 3. Show the result. Fulfil the order from the webhook, not from here.
if (result.status === 'succeeded') setMessage(`Thanks! Order ${orderId} is confirmed.`);
else if (result.status === 'failed') setMessage(result.error.message);
// 'canceled': the customer closed the sheet.
};
return (
<>
<PayWithStackButton onClick={() => void pay()} loading={loading} />
{message && <p>{message}</p>}
</>
);
}

presentPaymentSheet resolves when the sheet closes and never throws:

result.status What happened What to do
succeeded The customer paid. result.paymentIntentId is the payment. Show a confirmation. Fulfil from the webhook.
canceled The customer closed the sheet. Nothing.
failed Something stopped the payment. result.error.message says what. Show the message. The same payment can be tried again.

Browsers can’t hold a Face ID key, so on the web a returning customer confirms with their Stack PIN. A customer without a PIN gets a fresh email code. The first payment is confirmed by the sign-in code itself.

The sheet keeps the customer signed in to Stack on your site, in the browser’s storage for your publishable key. useStackSession lets you show it or sign out:

import { useStackSession } from '@usestack/react';
function Account() {
const { isConnected, contactMasked, signOut } = useStackSession();
if (!isConnected) return null;
return <button onClick={() => void signOut()}>Sign out of Stack ({contactMasked})</button>;
}
<StackProvider publishableKey="pk_test_..." appearance={{ accent: '#6d28d9', radius: 20 }}>

accent colours the sheet’s buttons and selection, and radius sets its corners.

A framework-free @usestack/web script is coming soon. Until then, @usestack/react works in any page that can render one React component.

See the @usestack/react reference for every prop.