Initialize the device for payments
Connect your device to the Stripe Terminal reader before accepting payments. This involves setting up a connection token provider and running the reader discovery flow.
Prerequisites
- The Stripe Terminal SDK installed in your app.
- A merchant profile with the status Enabled. If you don't have one yet, request a merchant profile first.
- An in-person card payment method with the status Enabled. If you don't have one yet, request the payment method first.
- A project access token, or a user access token with Can manage members rights.
Step 1: Implement a connection token provider
The Stripe Terminal SDK requires a connection token to connect to the reader.
You obtain this token from Swan's API using the requestTerminalConnectionToken mutation.
The SDK handles token refresh automatically. Do not cache the token yourself.
Get a connection token from Swan
Call requestTerminalConnectionToken from your server and pass the returned connectionToken to your provider.
mutation GetConnectionToken {
requestTerminalConnectionToken(
input: {
merchantProfileId: "$YOUR_MERCHANT_PROFILE_ID"
}
) {
... on RequestTerminalConnectionTokenSuccessPayload {
connectionToken
}
... on ForbiddenRejection {
__typename
message
}
... on InternalErrorRejection {
__typename
message
}
... on NotFoundRejection {
__typename
message
}
}
}
If you receive a ForbiddenRejection with the message Coming Soon, your project doesn't have the required feature flag.
Contact your Product Integration Manager or the Swan team to request access.
Wire up the provider
- React Native
- iOS
- Android
Pass a tokenProvider function as a prop to StripeTerminalProvider.
The function must return a connection token fetched from Swan's API.
import { StripeTerminalProvider } from '@stripe/stripe-terminal-react-native';
const fetchTokenProvider = async () => {
const connectionToken = "$YOUR_CONNECTION_TOKEN"; // fetch from Swan's API
return connectionToken;
};
function Root() {
return (
<StripeTerminalProvider
logLevel="verbose"
tokenProvider={fetchTokenProvider}
>
<App />
</StripeTerminalProvider>
);
}
Then call initialize from a component nested inside StripeTerminalProvider:
function App() {
const { initialize } = useStripeTerminal();
useEffect(() => {
initialize();
}, [initialize]);
return <View />;
}
Call initialize from a component nested inside StripeTerminalProvider, not from the component that contains the provider itself.
Implement the ConnectionTokenProvider protocol with a single fetchConnectionToken method:
import StripeTerminal
class SampleTokenProvider: ConnectionTokenProvider {
static let shared = SampleTokenProvider()
func fetchConnectionToken(_ completion: @escaping ConnectionTokenCompletionBlock) {
let connectionToken = "$YOUR_CONNECTION_TOKEN" // fetch from Swan's API
completion(connectionToken, nil)
// On error: completion(nil, error)
}
}
Initialize the terminal once in your AppDelegate, before first use:
import UIKit
import StripeTerminal
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
Terminal.setTokenProvider(SampleTokenProvider.shared)
return true
}
}
Implement the ConnectionTokenProvider interface with a single fetchConnectionToken method:
class SampleTokenProvider : ConnectionTokenProvider {
override fun fetchConnectionToken(callback: ConnectionTokenCallback) {
try {
val connectionToken = "$YOUR_CONNECTION_TOKEN" // fetch from Swan's API
callback.onSuccess(connectionToken)
} catch (e: Exception) {
callback.onFailure(
ConnectionTokenException("Failed to fetch connection token", e)
)
}
}
}
Tap to Pay on Android runs in a dedicated process. Skip initialization in that process by checking TapToPay.isInTapToPayProcess():
class StripeTerminalApplication : Application() {
override fun onCreate() {
super.onCreate()
if (TapToPay.isInTapToPayProcess()) return
TerminalApplicationDelegate.onCreate(this)
}
}
Then initialize the terminal with your token provider and a TerminalListener:
val listener = object : TerminalListener {
override fun onConnectionStatusChange(status: ConnectionStatus) {
println("Connection status: $status")
}
override fun onPaymentStatusChange(status: PaymentStatus) {
println("Payment status: $status")
}
}
val tokenProvider = SampleTokenProvider()
if (!Terminal.isInitialized()) {
Terminal.initTerminal(applicationContext, LogLevel.VERBOSE, tokenProvider, listener)
}
Step 2: Discover and connect to the reader
With the token provider configured, discover the device's reader and connect to it.
The Stripe Terminal SDK blocks reader connections in debug builds as a security measure. Use a simulated reader for development (see code examples below).
On iOS, the first time a user connects to a reader, Apple's Terms and Conditions screen appears automatically. This is handled by the SDK.
- React Native
- iOS
- Android
Use discoverReaders to find available readers, then connectReader to connect.
Set simulated: true during development.
export default function MainScreen() {
const [status, setStatus] = useState<Reader.ConnectionStatus>("notConnected");
const { discoverReaders, connectReader, connectedReader, cancelDiscovering } =
useStripeTerminal({
onDidChangeConnectionStatus: setStatus,
onUpdateDiscoveredReaders: async (readers: Reader.Type[]) => {
const tapToPayReader = readers.find(
(reader) => reader.deviceType === "tapToPay"
);
if (tapToPayReader != null) {
const { error } = await connectReader(
{
reader: tapToPayReader,
locationId: "$LOCATION_ID",
},
"tapToPay"
);
if (error != null) {
Alert.alert(error.code, error.message);
}
}
},
});
const hasConnectedReader = connectedReader != null;
useEffect(() => {
if (!hasConnectedReader) {
discoverReaders({
discoveryMethod: "tapToPay",
simulated: true, // set to false for production
}).then(({ error }) => {
if (error != null) {
Alert.alert(error.code, error.message);
}
});
return () => {
cancelDiscovering();
};
}
}, [hasConnectedReader, discoverReaders, cancelDiscovering]);
return <Text>{status}</Text>;
}
Create a TapToPayDiscoveryConfigurationBuilder and pass it to discoverReaders.
When a reader is found, connect to it using connectReader.
import StripeTerminal
class DiscoverReadersViewController: UIViewController, DiscoveryDelegate {
var discoverCancelable: Cancelable?
var tapToPayReaderDelegate: TapToPayReaderDelegate?
func discoverReaders() throws {
let config = try TapToPayDiscoveryConfigurationBuilder()
.setSimulated(true) // set to false for production
.build()
self.discoverCancelable = Terminal.shared.discoverReaders(
config,
delegate: self
) { error in
if let error = error {
print("discoverReaders failed: \(error)")
}
}
}
func terminal(_ terminal: Terminal, didUpdateDiscoveredReaders readers: [Reader]) {
guard let tapToPayReader = readers.first(where: { $0.deviceType == .tapToPay }) else { return }
let connectionConfig = try? TapToPayConnectionConfigurationBuilder
.init(locationId: "$LOCATION_ID")
// For simulated readers, use tapToPayReader.locationId instead
.delegate(tapToPayReaderDelegate)
.build()
Terminal.shared.connectReader(tapToPayReader, connectionConfig: connectionConfig!) { reader, error in
if let reader = reader {
print("Connected to reader: \(reader)")
} else if let error = error {
print("connectReader failed: \(error)")
}
}
}
}
Create a TapToPayDiscoveryConfiguration and pass it to discoverReaders.
Set isSimulated = true during development.
val discoverCancelable: Cancelable? = null
val tapToPayReaderListener: TapToPayReaderListener? = null
fun onDiscoverReaders() {
val config = TapToPayDiscoveryConfiguration(isSimulated = true) // set to false for production
discoverCancelable = Terminal.getInstance().discoverReaders(
config,
object : DiscoveryListener {
override fun onUpdateDiscoveredReaders(readers: List<Reader>) {
val tapToPayReader = readers.first {
it.deviceType == DeviceType.TAP_TO_PAY_DEVICE
}
val connectionConfig = TapToPayConnectionConfiguration(
"$LOCATION_ID",
// For simulated readers, use tapToPayReader.location?.id instead
autoReconnectOnUnexpectedDisconnect = true,
tapToPayReaderListener
)
Terminal.getInstance().connectReader(
tapToPayReader,
connectionConfig,
object : ReaderCallback {
override fun onSuccess(reader: Reader) { /* connected */ }
override fun onFailure(e: TerminalException) { /* handle error */ }
}
)
}
},
object : Callback {
override fun onSuccess() { /* discovery started */ }
override fun onFailure(e: TerminalException) { /* handle error */ }
}
)
}
override fun onStop() {
super.onStop()
// Cancel discovery if the user leaves without selecting a reader.
discoverCancelable?.cancel(object : Callback {
override fun onSuccess() {}
override fun onFailure(e: TerminalException) {}
})
}
The $LOCATION_ID in the code above is provided manually by Swan during the early access phase.
After you create a merchant profile and request Tap to Pay setup, your Product Integration Manager will share your location ID.
In a future release, it will be available as mainLocationId on the InPersonCardMerchantPaymentMethod object directly from the API.
Troubleshooting
Firewall or restricted network
If your app runs behind a firewall, configure it to allow access to the hosts required by Tap to Pay on iPhone. Refer to the Tap to Pay section of Apple's network configuration guide.
Reset Apple Terms and Conditions acceptance
The first time a user connects to a reader on iOS, Apple's Terms and Conditions screen appears automatically. Once accepted, it won't appear again for that user.
To reset acceptance for testing:
- Go to Apple Business Register for Tap to Pay on iPhone.
- Sign in with the Apple ID used to accept the Terms and Conditions.
- From the list of Merchant IDs, select Remove and confirm to unlink the Apple ID.
- The Terms and Conditions screen appears again on the next connection attempt.
If the user closes the Terms and Conditions screen without accepting, the connection fails with tapToPayReaderTOSAcceptanceFailed.
Prompt the user to accept and retry.
InternalErrorRejection when requesting a connection token
Merchant profile configuration is done manually during early access.
If requestTerminalConnectionToken is called before a profile is fully configured, the mutation returns an InternalErrorRejection.
Contact your Product Integration Manager with your merchant profile ID so they can complete the configuration.