Skip to content

Accept payments in React Native

@usestack/react-native gives you StackProvider, usePaymentSheet and the Pay with Stack button. The sheet is native: it slides up over your app, with no web view and no redirect.

Terminal window
npx expo install @usestack/react-native expo-secure-store expo-crypto expo-web-browser expo-clipboard react-native-svg

It works in Expo Go (Expo SDK 57): every native module it uses is included. In a bare React Native app, install the same packages and follow Expo’s instructions for adding Expo modules.

Add the Face ID permission text to app.json. Returning customers confirm payments with Face ID or a fingerprint:

app.json
{
"expo": {
"plugins": [
["expo-secure-store", { "faceIDPermission": "Your App uses Face ID to confirm Stack payments." }]
]
}
}
App.tsx
import { StackProvider } from '@usestack/react-native';
import { Checkout } from './Checkout';
export default function App() {
return (
// The publishable key is safe in the app. In development, apiUrl is your
// computer's LAN address (a phone can't reach "localhost").
<StackProvider publishableKey="pk_test_..." biometricPrompt="Confirm your payment">
<Checkout />
</StackProvider>
);
}
Checkout.tsx
import { PayWithStackButton, usePaymentSheet } from '@usestack/react-native';
import { Alert, View } from 'react-native';
const SERVER = 'https://your-server.example';
export function Checkout() {
const { presentPaymentSheet, loading } = usePaymentSheet();
const pay = async () => {
// 1. Your server creates the payment and returns its client secret.
const res = await fetch(`${SERVER}/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') Alert.alert('Order placed', `Order ${orderId} is confirmed.`);
else if (result.status === 'failed') Alert.alert('Payment failed', result.error.message);
// 'canceled': the customer closed the sheet.
};
return (
<View style={{ padding: 16 }}>
<PayWithStackButton onPress={() => void pay()} loading={loading} />
</View>
);
}

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: they’re back on your screen.
failed Something stopped the payment. result.error.message says what. Show the message. The same payment can be tried again.

A phone can’t reach localhost. Point the SDK at your computer’s address on the same Wi-Fi network:

<StackProvider publishableKey="pk_test_..." apiUrl="http://192.168.1.20:4000">

The same goes for your own server’s URL in fetch.

When a customer signs in to Stack in your app, the SDK creates a device key in the Keychain / Keystore, protected by biometrics. On later payments, the customer confirms by signing with that key, which shows the Face ID or fingerprint prompt. The key never leaves the device. The first payment is confirmed by the sign-in code itself.

  • No biometrics set up, or the prompt dismissed: the sheet asks for the customer’s Stack PIN instead.
  • No PIN yet: the sheet sends a fresh email code.
  • biometrics={false} turns device keys off.
  • biometricPrompt sets the text in the system prompt.

The sheet keeps the customer signed in to Stack in your app, in secure storage. useStackSession lets you show it or sign out:

import { useStackSession } from '@usestack/react-native';
function Account() {
const { isConnected, contactMasked, signOut } = useStackSession();
if (!isConnected) return null;
return <Button title={`Sign out of Stack (${contactMasked})`} onPress={() => void signOut()} />;
}

Signing out also forgets the device key.

<StackProvider publishableKey="pk_test_..." appearance={{ accent: '#FF5A1F', radius: 20 }}>

accent colours the sheet’s buttons and selection, and radius sets its corners. The Pay with Stack button keeps Stack’s look (variant="dark" or "light") so customers recognise it.

See the @usestack/react-native reference for every prop.