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.
Install
Section titled “Install”npm install @usestack/reactReact 18 or 19.
Add the provider and the button
Section titled “Add the provider and the button”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>,);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. |
How customers confirm on the web
Section titled “How customers confirm on the web”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.
Signed-in state
Section titled “Signed-in state”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.
Not using React?
Section titled “Not using React?”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.