Engineering article
Adding Tap to Pay (NFC Payments) in a React Native App using Stripe Terminal
In this article, we’ll walk through the end-to-end implementation of Tap‑to‑Pay in an existing React Native CLI project using Stripe Terminal.
What we are building
The goal is simple: allow a merchant to accept payments by letting customers tap their card on the device.
The payment can be made using:
- Physical debit cards
- Physical credit cards
- Contactless NFC cards
With Tap‑to‑Pay on iPhone, merchants can accept contactless payments directly from the iPhone without requiring additional hardware. The feature relies on the device’s NFC chip and secure payment infrastructure to process transactions safely.
Before moving forward, keep these points in mind:
- The SDK uses TypeScript features available in Babel version 7.9.0 and above.
- Android API level 26 and above.
compileSdkVersion = 35andtargetSdkVersion = 35.- Compatible with apps targeting iOS 15.1 or above.
1. Packages used
We’ll use the following packages:
@stripe/stripe-react-native@stripe/stripe-terminal-react-nativereact-native-nfc-manager
Example versions from package.json:
"@stripe/stripe-react-native": "0.50.3",
"@stripe/stripe-terminal-react-native": "0.0.1-beta.26",
"react-native-nfc-manager": "3.16.3",
"react-native": "0.81.5"
2. Required device permissions
iOS permissions
Add the following keys to your Info.plist:
NSLocationWhenInUseUsageDescriptionNSBluetoothAlwaysUsageDescriptionNSBluetoothPeripheralUsageDescriptionUIBackgroundModeswith valuebluetooth-central
Android permissions
Add the following permissions:
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECTPermissionsAndroid.PERMISSIONS.BLUETOOTH_SCANPermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
These permissions allow the app to discover and connect to nearby readers.
3. iOS setup — Tap to Pay on iPhone
Apple requires special approval to enable Tap‑to‑Pay. Follow Apple’s official “Setting up Tap to Pay” documentation and then:
- Request Tap‑to‑Pay entitlement from Apple Developer.
-
Go to
https://developer.apple.com/account/resources/identifiers, select your App ID and under Capabilities enable NFC Reading and Tap to Pay on iPhone. -
Use the Tap‑to‑Pay request form at
https://developer.apple.com/contact/request/tap-to-pay-on-iphone/. You need Admin access to submit it. - Create a new provisioning profile for the updated App ID and download it.
Configure the entitlement in your app
- Create a new entitlements file in Xcode (for example,
your_project.entitlements). - Set the Code Signing Entitlements build setting to point to that file.
-
Open the entitlements file and add the key
com.apple.developer.proximity-reader.payment.acceptancewith a Boolean value oftrue:
<key>com.apple.developer.proximity-reader.payment.acceptance</key>
<true/>
Tap‑to‑Pay requires an iPhone XS or later and a supported iOS version.
4. Create Stripe connection token
Stripe Terminal requires a short‑lived connection token, generated from your backend using the Stripe Secret Key.
Example cURL:
curl https://api.stripe.com/v1/terminal/connection_tokens \
-u sk_test_xxx: \
-X POST
Your backend should expose an endpoint such as:
POST /connection_token
5. Fetch token on the frontend
const fetchTokenProvider = async () => {
const response = await fetch(`${API_URL}/connection_token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const { secret } = await response.json();
return secret;
};
6. Initialize Stripe Terminal and SDK
import React, { useEffect } from "react";
import {
StripeTerminalProvider,
useStripeTerminal,
} from "@stripe/stripe-terminal-react-native";
function App() {
const { initialize } = useStripeTerminal();
// Call initialize on mount
useEffect(() => {
initialize();
}, [initialize]);
return (
<StripeTerminalProvider
logLevel="verbose"
tokenProvider={fetchTokenProvider}
>
<AppNavigator />
</StripeTerminalProvider>
);
}
7. Custom hook for Tap‑to‑Pay logic
To keep the screen clean, we move the payment logic into a custom hook,
useTapAndPayAction:
import {
PaymentIntent,
Reader,
requestNeededAndroidPermissions,
useStripeTerminal,
} from "@stripe/stripe-terminal-react-native";
import { useEffect, useState } from "react";
import NfcManager from "react-native-nfc-manager";
NfcManager.start();
export const useTapAndPayAction = () => {
const [payment, setPayment] = useState();
const [paymentProcessing, setPaymentProcessing] = useState(true);
const [locationIdText, setLocationIdText] = useState("");
const {
isInitialized,
initialize,
discoverReaders,
connectReader,
disconnectReader,
cancelDiscovering,
retrievePaymentIntent,
collectPaymentMethod,
confirmPaymentIntent,
} = useStripeTerminal({
onUpdateDiscoveredReaders: (readers) => {
handleConnectReader(readers[0]);
},
});
useEffect(() => {
checkNfcSupport();
}, []);
const checkNfcSupport = async () => {
const supported = await NfcManager.isSupported();
const enabled = await NfcManager.isEnabled();
console.log("supported =>", supported);
console.log("enabled =>", enabled);
};
useEffect(() => {
initialize();
}, []);
useEffect(() => {
fetchReaders();
}, [isInitialized]);
const fetchReaders = async () => {
if (isInitialized) {
await discoverReaders({
discoveryMethod: "tapToPay",
simulated: false,
});
}
};
async function handleConnectReader(selectedReader) {
const { reader, error } = await connectReader(
{
reader: selectedReader,
locationId: locationIdText,
autoReconnectOnUnexpectedDisconnect: true,
},
"tapToPay"
);
_fetchPaymentIntent();
}
async function _fetchPaymentIntent() {
const { paymentIntent } = await retrievePaymentIntent(
paymentData.paymentIntentData.client_secret
);
setPayment(paymentIntent);
}
async function collectPayment(pi) {
const { paymentIntent } = await collectPaymentMethod({
paymentIntent: pi,
});
_confirmPaymentIntent(paymentIntent);
}
const _confirmPaymentIntent = async (collectedPaymentIntent) => {
const { paymentIntent } = await confirmPaymentIntent({
paymentIntent: collectedPaymentIntent,
});
if (paymentIntent?.status === "succeeded") {
console.log("Payment successful");
}
};
return {
paymentProcessing,
payment,
collectPayment,
};
};
8. Using the hook in a screen
const TapAndPayScreen = () => {
const {
paymentProcessing,
payment,
collectPayment,
} = useTapAndPayAction();
const _handleTapToPayClick = () => {
collectPayment(payment);
};
return (
<View>
{/* Other components */}
<Button
title="Tap To Pay"
onPress={_handleTapToPayClick}
/>
</View>
);
};
Wrap‑up
Congratulations! Your React Native app now supports Tap to Pay using Stripe Terminal and NFC. With the right permissions, entitlements, and Stripe configuration, you can provide a hardware‑free contactless payment experience to your merchants.
Share your thoughts
Have questions or ideas about Tap to Pay or Stripe Terminal? Leave a comment below.