From 55133c58f40feebc21bcdcf550c2a5d1e8a8dd61 Mon Sep 17 00:00:00 2001 From: Maxence Busson Date: Fri, 6 Mar 2026 16:27:22 +0100 Subject: [PATCH 1/4] DOC-1585/ttp-step-3-sandbox-and-rename --- docs/preview/in-person/cards/guide-accept.mdx | 25 +- .../in-person/cards/guide-create-payments.mdx | 66 +--- .../in-person/cards/guide-initialize.mdx | 368 ++++++++++++++++++ docs/preview/in-person/cards/guide-setup.mdx | 235 +++++++++++ docs/preview/in-person/cards/index.mdx | 4 +- sidebars.js | 2 + 6 files changed, 644 insertions(+), 56 deletions(-) create mode 100644 docs/preview/in-person/cards/guide-initialize.mdx create mode 100644 docs/preview/in-person/cards/guide-setup.mdx diff --git a/docs/preview/in-person/cards/guide-accept.mdx b/docs/preview/in-person/cards/guide-accept.mdx index 2b8c1b73bc..5c6755635c 100644 --- a/docs/preview/in-person/cards/guide-accept.mdx +++ b/docs/preview/in-person/cards/guide-accept.mdx @@ -9,28 +9,35 @@ This section only applies to **merchants** accepting card payments. Visit the [card payments section](../../../topics/payments/cards/index.mdx) for information about payments made with cards *from* a Swan account. ::: -## Step 1: Request a merchant profile {#step-1} +## Step 1: Set up the development environment {#step-1} + +Install the Stripe Terminal SDK and configure your app with the required Apple entitlements and permissions. + +→ [Set up the development environment](./guide-setup.mdx) + +## Step 2: Request a merchant profile {#step-2} Before accepting in-person card payments, you need a merchant profile. → [Request a merchant profile](../../../topics/merchants/profiles/guide-request.mdx) -You can also update merchant profiles if needed. +## Step 3: Request the payment method {#step-3} -→ [Update a merchant profile](../../../topics/merchants/profiles/guide-update.mdx) +→ [Request the in-person card payment method](./guide-request-method.mdx) -## Step 2: Request the payment method {#step-2} +## Step 4: Initialize the device {#step-4} -→ [Request the in-person card payment method](./guide-request-method.mdx) +Set up the connection token provider and connect your device to the Stripe Terminal reader. + +→ [Initialize the device for payments](./guide-initialize.mdx) -## Step 3: Create in-person card payments {#step-3} +## Step 5: Create in-person card payments {#step-5} -Call the API to create payment intents. -This lets merchants accept card payments from customers using their terminal app. +Call the API to create payment intents and use the Stripe SDK to process payments. → [Create in-person card payments](./guide-create-payments.mdx) -## Step 4: Issue a refund {#step-4} +## Step 6: Issue a refund {#step-6} import InPersonCardRefundLimitation from '../partials/_in-person-card-refund-limitation.mdx'; diff --git a/docs/preview/in-person/cards/guide-create-payments.mdx b/docs/preview/in-person/cards/guide-create-payments.mdx index 70c8323b42..fa70bd200c 100644 --- a/docs/preview/in-person/cards/guide-create-payments.mdx +++ b/docs/preview/in-person/cards/guide-create-payments.mdx @@ -8,38 +8,12 @@ Create payment intents so your merchants can accept card payments from customers ## Prerequisites {#prerequisites} +- The [Stripe Terminal SDK](./guide-setup.mdx) installed and the [device initialized](./guide-initialize.mdx). - A **merchant profile** with the status **Enabled**. - An **in-person card payment method** with the status **Enabled**. If you don't have one yet, [request the payment method](./guide-request-method.mdx) first. - A project access token, or a user access token with **Can manage members** rights. -- The [Stripe Terminal SDK](https://docs.stripe.com/terminal/payments/setup-sdk) integrated in your terminal app. -## Step 1: Get a connection token {#connection-token} - -Get a connection token from Stripe so the terminal app can discover and connect to a reader - -Call the `requestTerminalConnectionToken` mutation from your server. -Then, pass the returned `connectionToken` to the `StripeTerminalProvider` in your terminal app. - -```graphql -mutation GetConnectionToken { - requestTerminalConnectionToken( - input: { - merchantProfileId: "$YOUR_MERCHANT_PROFILE_ID" - } - ) { - ... on RequestInPersonTerminalConnectionTokenSuccessPayload { - connectionToken - } - } -} -``` - -:::info Token lifecycle -Connection tokens are short-lived. -Your app should request a new token each time the Stripe SDK needs one, typically by providing a token fetch function to the `StripeTerminalProvider`. -::: - -## Step 2: Create a payment intent {#create-intent} +## Step 1: Create a payment intent {#create-intent} Call the `createInPersonPaymentIntent` mutation to create a payment intent. The mutation returns a `secret` (Stripe's client secret) that you'll use in the next step with the Stripe SDK. @@ -71,36 +45,38 @@ mutation CreateInPersonPayment { } ``` -### Mutation input {#mutation-input} +### Mutation fields {#mutation-input} -| Field | Type | Required | Description | + + +| Field | Type | | Description | | --- | --- | --- | --- | -| `merchantProfileId` | `ID` | ✅ | ID of the merchant profile accepting the payment. | -| `amount` | `AmountInput` | ✅ | Amount for the payment intent. Minimum: €0.50. | -| `label` | `String` | No | Label shown on the merchant's bank statement. | -| `externalReference` | `String` | No | Reference to match the payment with your external system (for example, an order ID). | -| `reference` | `String` | No | Payment reference. | -| `idempotencyKey` | `String` | No | Unique key to prevent duplicate payment creation. | +| `merchantProfileId` | `ID` | | ID of the merchant profile accepting the payment. | +| `amount` | `AmountInput` | | Amount for the payment intent. Minimum: €0.50. | +| `label` | `String` | | Label shown on the merchant's bank statement. | +| `externalReference` | `String` | | Reference to match the payment with an external system, such as an order ID. | +| `reference` | `String` | | Payment reference. | +| `idempotencyKey` | `String` | | Unique key to prevent duplicate payment creation. | -## Step 3: Process the payment with Stripe SDK {#process-payment} +## Step 2: Process the payment with the Stripe SDK {#process-payment} After creating the payment intent, use the Stripe Terminal SDK in your terminal app to collect and confirm the payment. -This step happens entirely on the device. +This step runs entirely on the device. :::tip -For detailed SDK integration guidance, refer to the [Stripe Terminal: Collect card payment](https://docs.stripe.com/terminal/payments/collect-card-payment) documentation. +For full SDK integration guidance, refer to [Stripe Terminal: Collect card payment](https://docs.stripe.com/terminal/payments/collect-card-payment). ::: -### 3.1: Retrieve the payment intent {#retrieve-intent} +### 2.1: Retrieve the payment intent {#retrieve-intent} -Use the `secret` returned in step 2 to retrieve the payment intent with the Stripe SDK's `retrievePaymentIntent` method. +Use the `secret` from step 1 to retrieve the payment intent with the Stripe SDK's `retrievePaymentIntent` method. -### 3.2: Collect the payment method {#collect-method} +### 2.2: Collect the payment method {#collect-method} Call the Stripe SDK's `collectPaymentMethod` method. This activates the device's NFC reader and prompts the customer to tap their card. -### 3.3: Confirm the payment intent {#confirm-intent} +### 2.3: Confirm the payment intent {#confirm-intent} Call the Stripe SDK's `confirmPaymentIntent` method to finalize the payment. The payment is captured automatically. @@ -110,11 +86,9 @@ The payment is captured automatically. After processing, the merchant payment object reflects the payment's lifecycle. Refer to the [payment object statuses](./index.mdx#payment-statuses) for the full list. -These are the key statuses after a payment is processed: - | Status | Explanation | | --- | --- | -| `Captured` | Payment authorized and captured successfully. Funds will be [settled](./index.mdx#settlement) to the merchant's account. | +| `Captured` | Payment authorized and captured. Funds will be [settled](./index.mdx#settlement) to the merchant's account. | | `Rejected` | Payment declined by the issuer or by Swan. Refer to [rejection reasons](./index.mdx#rejected) for details. | | `Disputed` | Customer disputed the payment for some or all of the amount. | | `Refunded` | Payment reversed by the merchant for some or all of the amount. | diff --git a/docs/preview/in-person/cards/guide-initialize.mdx b/docs/preview/in-person/cards/guide-initialize.mdx new file mode 100644 index 0000000000..34c765c75f --- /dev/null +++ b/docs/preview/in-person/cards/guide-initialize.mdx @@ -0,0 +1,368 @@ +--- +title: Initialize the device for payments +--- + +# 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 {#prerequisites} + +- The [Stripe Terminal SDK](./guide-setup.mdx) installed in your app. +- A **merchant profile** with the status **Enabled**. If you don't have one yet, [request a merchant profile](../../../topics/merchants/profiles/guide-request.mdx) first. +- An **in-person card payment method** with the status **Enabled**. If you don't have one yet, [request the payment method](./guide-request-method.mdx) first. +- A project access token, or a user access token with **Can manage members** rights. + +## Step 1: Implement a connection token provider {#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. + +First, implement a connection token provider that calls Swan's API. + +### Get a connection token from Swan {#get-token} + +Call `requestTerminalConnectionToken` from your server and pass the returned `connectionToken` to your provider. + +```graphql +mutation GetConnectionToken { + requestTerminalConnectionToken( + input: { + merchantProfileId: "$YOUR_MERCHANT_PROFILE_ID" + } + ) { + ... on RequestInPersonTerminalConnectionTokenSuccessPayload { + connectionToken + } + ... on ForbiddenRejection { + __typename + message + } + } +} +``` + +:::caution Early access only +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 {#wire-provider} + + + + +Pass a `tokenProvider` function as a prop to `StripeTerminalProvider`. +The function must return a connection token fetched from Swan's API. + +```tsx +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 ( + + + + ); +} +``` + +Then call `initialize` from a component nested inside `StripeTerminalProvider`: + +```tsx +function App() { + const { initialize } = useStripeTerminal(); + + useEffect(() => { + initialize(); + }, [initialize]); + + return ; +} +``` + +:::note +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: + +```swift +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: + +```swift +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: + +```kotlin +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()`: + +```kotlin +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`: + +```kotlin +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 {#connect-reader} + +With the token provider configured, discover the device's reader and connect to it. + +:::note Debug mode +The Stripe Terminal SDK blocks reader connections in debug builds as a security measure. +Use a simulated reader for development (see code examples below). +::: + +:::info iOS Terms and Conditions +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. +::: + + + + +Use `discoverReaders` to find available readers, then `connectReader` to connect. +Set `simulated: true` during development. + +```tsx +export default function MainScreen() { + const [status, setStatus] = useState("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 {status}; +} +``` + + + + +Create a `TapToPayDiscoveryConfigurationBuilder` and pass it to `discoverReaders`. +When a reader is found, connect to it using `connectReader`. + +```swift +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") + .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. + +```kotlin +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) { + val tapToPayReader = readers.first { + it.deviceType == DeviceType.TAP_TO_PAY_DEVICE + } + + val connectionConfig = TapToPayConnectionConfiguration( + "$LOCATION_ID", + 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) {} + }) +} +``` + + + + +:::caution Location ID: temporary during early access +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. +This value will be available directly through the API in a future release. +::: + +## Next steps {#next-steps} + +→ [Create in-person card payments](./guide-create-payments.mdx) \ No newline at end of file diff --git a/docs/preview/in-person/cards/guide-setup.mdx b/docs/preview/in-person/cards/guide-setup.mdx new file mode 100644 index 0000000000..3d820504a0 --- /dev/null +++ b/docs/preview/in-person/cards/guide-setup.mdx @@ -0,0 +1,235 @@ +--- +title: Set up the development environment +--- + +# Set up the development environment + +Prepare your development environment before building your in-person card payment integration. + +## Prerequisites {#prerequisites} + +- An Apple Developer account with organization-level access (iOS only). +- A React Native, iOS, or Android project. + +## Step 1: Request Apple entitlements (iOS only) {#entitlements} + +:::info iOS only +Skip this step if you're building for Android only. +::: + +To develop Tap to Pay on iPhone, you need a Tap to Pay on iPhone entitlement from Apple. +This applies even if you're developing with React Native. + +1. Sign in to your Apple Developer account as the Account Holder. +1. Submit a request using [Apple's entitlement request form](https://developer.apple.com/contact/request/tap-to-pay-on-iphone/). +1. On the form, provide the following: + - **Your PSP**: Stripe. + - **Distributed apps using this entitlement**: the number of existing apps that will include Tap to Pay. + - **New apps using this entitlement**: the number of new apps you plan to launch with Tap to Pay. + - **Distribution method**: public through the App Store. + - Fill in the remaining fields based on your use case: expected number of users, target countries, and expected release date. +1. After submitting, Apple sends a confirmation email. Follow Apple's guide to add the Tap to Pay capability to your App ID, download the provisioning profile, and configure the entitlements file. + +:::caution Save your confirmation email +The entitlement received allows development and internal testing only. +Before publishing to the App Store or TestFlight, reply to this email to request the production entitlement, then resubmit the form. +::: + +## Step 2: Install the Stripe Terminal SDK {#install-sdk} + +Swan uses Stripe as its provider for in-person card payments. +Install the Stripe Terminal SDK in your project. + +:::note Stripe docs reference +Follow the steps in this guide when installing the SDK. +Steps 3 and 4 in [Stripe's documentation](https://docs.stripe.com/terminal/payments/setup-sdk) differ for Swan integrations — refer to this guide instead. +::: + + + + +The React Native SDK is open source and available on [GitHub](https://github.com/stripe/stripe-terminal-react-native). + +Install it using your package manager: + +```bash +# npm +npm install @stripe/stripe-terminal-react-native + +# Yarn +yarn add @stripe/stripe-terminal-react-native + +# Expo +npx expo install @stripe/stripe-terminal-react-native +``` + + + + +The Stripe Terminal iOS SDK requires iOS 13 or later. + + + + +Add the following line to your `Podfile`, then run `pod install`: + +```ruby +pod 'StripeTerminal', '~> 4.0' +``` + + + + +1. In Xcode, select **File** > **Add Packages…**. +1. Enter the SDK URL: `https://github.com/stripe/stripe-terminal-ios` +1. Select a version. The default **Up to Next Major** is recommended to receive security and feature updates without unexpected breaking changes. + + + + +1. Go to the [latest release](https://github.com/stripe/stripe-terminal-ios/releases) on GitHub. +1. Download `StripeTerminal.xcframework.zip` and unzip it. +1. Drag the `.xcframework` into your Xcode project. +1. In your target's **General** pane, under **Frameworks, Libraries, and Embedded Content**, set `StripeTerminal.xcframework` to **Embed and Sign**. + + + + + + + +The Android SDK requires AndroidX. Add `stripeterminal` to your app's `build.gradle` dependencies: + + + + +```kotlin +plugins { + id("com.android.application") +} + +android { ... } + +dependencies { + implementation("com.stripe:stripeterminal:4.7.3") +} +``` + +The SDK requires Java 8. Set your target Java version: + +```kotlin +android { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} +``` + + + + +```groovy +apply plugin: 'com.android.application' + +android { ... } + +dependencies { + implementation "com.stripe:stripeterminal:4.7.3" +} +``` + +The SDK requires Java 8. Set your target Java version: + +```groovy +compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 +} +``` + + + + + + + +## Step 3: Request location permissions {#permissions} + +The Stripe Terminal SDK requires location access when running in the foreground. +It uses this to determine where payments are made and reduce fraud risks. +The SDK won't allow payments without this access. + +:::tip UX best practice +Before requesting location access, explain to users why you need it. +A brief in-app message before the system prompt improves acceptance rates. +::: + + + + +Location permission configuration differs by target OS and whether you use React Native CLI or Expo. + +Follow [Step 2 (Configure your app)](https://docs.stripe.com/terminal/payments/setup-reader/tap-to-pay?terminal-sdk-platform=react-native#configure-your-app) in Stripe's guide. +Do not continue past that step — the remaining steps differ for Swan integrations. + + + + +Add the following key-value pair to your `Info.plist`: + +```xml +NSLocationWhenInUseUsageDescription +Location access is required to accept payments. +``` + + + + +Add the location permission to your `AndroidManifest.xml`: + +```xml + +``` + +Verify that location access has been granted before using the SDK. +In your main activity: + +```kotlin +if (ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED +) { + val permissions = arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION) + ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE_LOCATION) +} +``` + +Override `onRequestPermissionsResult` to handle the user's response: + +```kotlin +override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray +) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == REQUEST_CODE_LOCATION + && grantResults.isNotEmpty() + && grantResults[0] != PackageManager.PERMISSION_GRANTED + ) { + // Location permission denied — the SDK won't allow payments without it. + } +} +``` + + + + +## Next steps {#next-steps} + +→ [Request a merchant profile](../../../topics/merchants/profiles/guide-request.mdx) \ No newline at end of file diff --git a/docs/preview/in-person/cards/index.mdx b/docs/preview/in-person/cards/index.mdx index 0d0ee60268..53dc1b5faf 100644 --- a/docs/preview/in-person/cards/index.mdx +++ b/docs/preview/in-person/cards/index.mdx @@ -152,8 +152,10 @@ To respond to disputes, ask your merchant to [submit a request](https://supportf ## Guides {#guides} -- [Accept in-person payments with cards](./guide-accept.mdx) +- [Accept in-person card payments](./guide-accept.mdx) +- [Set up the development environment](./guide-setup.mdx) - [Request in-person card payment method](./guide-request-method.mdx) +- [Initialize the device for payments](./guide-initialize.mdx) - [Create in-person card payments](./guide-create-payments.mdx) :::note Sandbox coming soon diff --git a/sidebars.js b/sidebars.js index b1e2a77f1e..f63841bed6 100644 --- a/sidebars.js +++ b/sidebars.js @@ -788,7 +788,9 @@ module.exports = { collapsed: true, items: [ "preview/in-person/cards/guide-accept", + "preview/in-person/cards/guide-setup", "preview/in-person/cards/guide-request-method", + "preview/in-person/cards/guide-initialize", "preview/in-person/cards/guide-create-payments", ], }, From 0cccf6166e55ead037180fc0917666ee1087c022 Mon Sep 17 00:00:00 2001 From: Maxence Busson Date: Fri, 6 Mar 2026 17:06:24 +0100 Subject: [PATCH 2/4] Proofreading --- docs/preview/in-person/cards/guide-accept.mdx | 2 ++ .../in-person/cards/guide-create-payments.mdx | 2 +- .../in-person/cards/guide-initialize.mdx | 4 +--- docs/preview/in-person/cards/guide-setup.mdx | 19 +++++++++---------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/preview/in-person/cards/guide-accept.mdx b/docs/preview/in-person/cards/guide-accept.mdx index 5c6755635c..668dc32bc0 100644 --- a/docs/preview/in-person/cards/guide-accept.mdx +++ b/docs/preview/in-person/cards/guide-accept.mdx @@ -23,6 +23,8 @@ Before accepting in-person card payments, you need a merchant profile. ## Step 3: Request the payment method {#step-3} +Request the in-person card payment method to enable your merchants to accept card payments. + → [Request the in-person card payment method](./guide-request-method.mdx) ## Step 4: Initialize the device {#step-4} diff --git a/docs/preview/in-person/cards/guide-create-payments.mdx b/docs/preview/in-person/cards/guide-create-payments.mdx index fa70bd200c..df3e1243b7 100644 --- a/docs/preview/in-person/cards/guide-create-payments.mdx +++ b/docs/preview/in-person/cards/guide-create-payments.mdx @@ -96,7 +96,7 @@ Refer to the [payment object statuses](./index.mdx#payment-statuses) for the ful ## After the payment {#after-payment} After the payment is captured, the merchant payment object is updated and underlying transactions are created. -Refer to the [card transaction types](./index.mdx#transaction-types) section to understand how transactions are created and settled. +Refer to [card transaction types](./index.mdx#transaction-types) to understand how they settle. import InPersonCardRefundLimitation from '../partials/_in-person-card-refund-limitation.mdx'; diff --git a/docs/preview/in-person/cards/guide-initialize.mdx b/docs/preview/in-person/cards/guide-initialize.mdx index 34c765c75f..262ad0991a 100644 --- a/docs/preview/in-person/cards/guide-initialize.mdx +++ b/docs/preview/in-person/cards/guide-initialize.mdx @@ -18,9 +18,7 @@ This involves setting up a connection token provider and running the reader disc 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. - -First, implement a connection token provider that calls Swan's API. +The SDK handles token refresh automatically. Do not cache the token yourself. ### Get a connection token from Swan {#get-token} diff --git a/docs/preview/in-person/cards/guide-setup.mdx b/docs/preview/in-person/cards/guide-setup.mdx index 3d820504a0..7f50a7d559 100644 --- a/docs/preview/in-person/cards/guide-setup.mdx +++ b/docs/preview/in-person/cards/guide-setup.mdx @@ -17,8 +17,8 @@ Prepare your development environment before building your in-person card payment Skip this step if you're building for Android only. ::: -To develop Tap to Pay on iPhone, you need a Tap to Pay on iPhone entitlement from Apple. -This applies even if you're developing with React Native. +To develop Tap to Pay on iPhone, you need the Tap to Pay on iPhone entitlement from Apple. +It applies even if you're developing with React Native. 1. Sign in to your Apple Developer account as the Account Holder. 1. Submit a request using [Apple's entitlement request form](https://developer.apple.com/contact/request/tap-to-pay-on-iphone/). @@ -31,7 +31,7 @@ This applies even if you're developing with React Native. 1. After submitting, Apple sends a confirmation email. Follow Apple's guide to add the Tap to Pay capability to your App ID, download the provisioning profile, and configure the entitlements file. :::caution Save your confirmation email -The entitlement received allows development and internal testing only. +This entitlement covers development and internal testing only. Before publishing to the App Store or TestFlight, reply to this email to request the production entitlement, then resubmit the form. ::: @@ -41,8 +41,8 @@ Swan uses Stripe as its provider for in-person card payments. Install the Stripe Terminal SDK in your project. :::note Stripe docs reference -Follow the steps in this guide when installing the SDK. -Steps 3 and 4 in [Stripe's documentation](https://docs.stripe.com/terminal/payments/setup-sdk) differ for Swan integrations — refer to this guide instead. +Follow this guide to install the SDK. +Steps 3 and 4 in [Stripe's documentation](https://docs.stripe.com/terminal/payments/setup-sdk) differ for Swan integrations. ::: @@ -159,9 +159,8 @@ compileOptions { ## Step 3: Request location permissions {#permissions} -The Stripe Terminal SDK requires location access when running in the foreground. -It uses this to determine where payments are made and reduce fraud risks. -The SDK won't allow payments without this access. +The Stripe Terminal SDK requires foreground location access to determine where payments are made and reduce fraud risks. +Payments won't start without it. :::tip UX best practice Before requesting location access, explain to users why you need it. @@ -174,7 +173,7 @@ A brief in-app message before the system prompt improves acceptance rates. Location permission configuration differs by target OS and whether you use React Native CLI or Expo. Follow [Step 2 (Configure your app)](https://docs.stripe.com/terminal/payments/setup-reader/tap-to-pay?terminal-sdk-platform=react-native#configure-your-app) in Stripe's guide. -Do not continue past that step — the remaining steps differ for Swan integrations. +Do not continue past that step. The remaining steps differ for Swan integrations. @@ -222,7 +221,7 @@ override fun onRequestPermissionsResult( && grantResults.isNotEmpty() && grantResults[0] != PackageManager.PERMISSION_GRANTED ) { - // Location permission denied — the SDK won't allow payments without it. + // Location permission denied. The SDK won't allow payments without it. } } ``` From 9ce2b90d28e2ecb94c4fda3b9f377d9703cbdc7a Mon Sep 17 00:00:00 2001 From: Maxence Busson Date: Fri, 6 Mar 2026 17:25:11 +0100 Subject: [PATCH 3/4] Fixing elements after cross-reference --- docs/preview/in-person/cards/guide-accept.mdx | 7 + .../in-person/cards/guide-create-payments.mdx | 167 +++++++++++++++++- .../in-person/cards/guide-initialize.mdx | 40 ++++- docs/preview/in-person/cards/index.mdx | 25 +++ 4 files changed, 230 insertions(+), 9 deletions(-) diff --git a/docs/preview/in-person/cards/guide-accept.mdx b/docs/preview/in-person/cards/guide-accept.mdx index 668dc32bc0..8e08b7baff 100644 --- a/docs/preview/in-person/cards/guide-accept.mdx +++ b/docs/preview/in-person/cards/guide-accept.mdx @@ -19,6 +19,13 @@ Install the Stripe Terminal SDK and configure your app with the required Apple e Before accepting in-person card payments, you need a merchant profile. +:::caution Early access: manual profile setup +During this phase, Swan manually sets up merchant profiles with the Stripe connection. +After requesting a profile, contact your Product Integration Manager with your merchant profile ID. +They will confirm when setup is complete and provide your `$LOCATION_ID`. +Swan can configure up to 10 profiles per partner during early access. +::: + → [Request a merchant profile](../../../topics/merchants/profiles/guide-request.mdx) ## Step 3: Request the payment method {#step-3} diff --git a/docs/preview/in-person/cards/guide-create-payments.mdx b/docs/preview/in-person/cards/guide-create-payments.mdx index df3e1243b7..3551f8f1db 100644 --- a/docs/preview/in-person/cards/guide-create-payments.mdx +++ b/docs/preview/in-person/cards/guide-create-payments.mdx @@ -63,23 +63,174 @@ mutation CreateInPersonPayment { After creating the payment intent, use the Stripe Terminal SDK in your terminal app to collect and confirm the payment. This step runs entirely on the device. -:::tip -For full SDK integration guidance, refer to [Stripe Terminal: Collect card payment](https://docs.stripe.com/terminal/payments/collect-card-payment). -::: - ### 2.1: Retrieve the payment intent {#retrieve-intent} -Use the `secret` from step 1 to retrieve the payment intent with the Stripe SDK's `retrievePaymentIntent` method. +Use the `secret` from step 1 to retrieve the payment intent with the Stripe SDK. + + + + +```tsx +const secret = "$PAYMENT_INTENT_SECRET"; // from step 1 +const { paymentIntent, error } = await retrievePaymentIntent(secret); +if (error) { + // Handle error + return; +} +``` + + + + +```swift +let secret = "$PAYMENT_INTENT_SECRET" // from step 1 +Terminal.shared.retrievePaymentIntent(clientSecret: secret) { retrieveResult, retrieveError in + if let error = retrieveError { + print("retrievePaymentIntent failed: \(error)") + } else if let paymentIntent = retrieveResult { + // Proceed to collect payment method + } +} +``` + + + + +```kotlin +val secret = "$PAYMENT_INTENT_SECRET" // from step 1 +Terminal.getInstance().retrievePaymentIntent( + secret, + object : PaymentIntentCallback { + override fun onSuccess(paymentIntent: PaymentIntent) { + // Proceed to collect payment method + } + override fun onFailure(e: TerminalException) { + // Handle error + } + } +) +``` + + + ### 2.2: Collect the payment method {#collect-method} -Call the Stripe SDK's `collectPaymentMethod` method. +Call `collectPaymentMethod` to show the Tap to Pay screen. This activates the device's NFC reader and prompts the customer to tap their card. + + + +```tsx +const { paymentIntent, error } = await collectPaymentMethod({ + paymentIntent: paymentIntent, + enableCustomerCancellation: true, +}); +if (error) { + // Handle error +} +``` + + + + +```swift +let collectConfig = try CollectConfigurationBuilder() + .setEnableCustomerCancellation(true) + .build() + +self.collectCancelable = Terminal.shared.collectPaymentMethod( + paymentIntent: paymentIntent, + collectConfig: collectConfig +) { collectResult, collectError in + if let error = collectError { + print("collectPaymentMethod failed: \(error)") + } else if let paymentIntent = collectResult { + // Proceed to confirm + } +} +``` + + + + +```kotlin +val cancelable = Terminal.getInstance().collectPaymentMethod( + paymentIntent, + object : PaymentIntentCallback { + override fun onSuccess(paymentIntent: PaymentIntent) { + // Proceed to confirm + } + override fun onFailure(e: TerminalException) { + // Handle error + } + }, + CollectConfiguration.Builder() + .setEnableCustomerCancellation(true) + .build() +) +``` + + + + ### 2.3: Confirm the payment intent {#confirm-intent} -Call the Stripe SDK's `confirmPaymentIntent` method to finalize the payment. -The payment is captured automatically. +Call `confirmPaymentIntent` to finalize the payment. +The payment is authorized and captured automatically. + +:::tip Retrying a failed capture +If capture fails and you want to offer a retry (for example, with a different card), reuse the same payment intent. +Repeat step 2.2, then attempt to confirm again. +::: + + + + +```tsx +const { paymentIntent, error } = await confirmPaymentIntent({ + paymentIntent: paymentIntent, +}); +if (error) { + // Handle error +} +``` + + + + +```swift +self.confirmCancelable = Terminal.shared.confirmPaymentIntent( + paymentIntent +) { confirmResult, confirmError in + if let error = confirmError { + print("confirmPaymentIntent failed: \(error)") + } else if let confirmedPaymentIntent = confirmResult { + print("confirmPaymentIntent succeeded") + } +} +``` + + + + +```kotlin +val cancelable = Terminal.getInstance().confirmPaymentIntent( + paymentIntent, + object : PaymentIntentCallback { + override fun onSuccess(paymentIntent: PaymentIntent) { + // Payment confirmed + } + override fun onFailure(e: TerminalException) { + // Handle error + } + } +) +``` + + + ## Merchant payment statuses {#statuses} diff --git a/docs/preview/in-person/cards/guide-initialize.mdx b/docs/preview/in-person/cards/guide-initialize.mdx index 262ad0991a..ed38a88d84 100644 --- a/docs/preview/in-person/cards/guide-initialize.mdx +++ b/docs/preview/in-person/cards/guide-initialize.mdx @@ -31,13 +31,21 @@ mutation GetConnectionToken { merchantProfileId: "$YOUR_MERCHANT_PROFILE_ID" } ) { - ... on RequestInPersonTerminalConnectionTokenSuccessPayload { + ... on RequestTerminalConnectionTokenSuccessPayload { connectionToken } ... on ForbiddenRejection { __typename message } + ... on InternalErrorRejection { + __typename + message + } + ... on NotFoundRejection { + __typename + message + } } } ``` @@ -284,6 +292,7 @@ class DiscoverReadersViewController: UIViewController, DiscoveryDelegate { let connectionConfig = try? TapToPayConnectionConfigurationBuilder .init(locationId: "$LOCATION_ID") + // For simulated readers, use tapToPayReader.locationId instead .delegate(tapToPayReaderDelegate) .build() @@ -321,6 +330,7 @@ fun onDiscoverReaders() { val connectionConfig = TapToPayConnectionConfiguration( "$LOCATION_ID", + // For simulated readers, use tapToPayReader.location?.id instead autoReconnectOnUnexpectedDisconnect = true, tapToPayReaderListener ) @@ -361,6 +371,34 @@ After you create a merchant profile and request Tap to Pay setup, your Product I This value will be available directly through the API in a future release. ::: +## Troubleshooting {#troubleshooting} + +### Firewall or restricted network {#firewall} + +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](https://support.apple.com/en-us/101555). + +### Reset Apple Terms and Conditions acceptance {#reset-tac} + +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: + +1. Go to [Apple Business Register for Tap to Pay on iPhone](https://register.apple.com/tap-to-pay-on-iphone/). +1. Sign in with the Apple ID used to accept the Terms and Conditions. +1. From the list of Merchant IDs, select **Remove** and confirm to unlink the Apple ID. +1. 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 {#internal-error} + +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. + ## Next steps {#next-steps} → [Create in-person card payments](./guide-create-payments.mdx) \ No newline at end of file diff --git a/docs/preview/in-person/cards/index.mdx b/docs/preview/in-person/cards/index.mdx index 53dc1b5faf..fb43235cd9 100644 --- a/docs/preview/in-person/cards/index.mdx +++ b/docs/preview/in-person/cards/index.mdx @@ -150,6 +150,31 @@ However, it's not possible to act on disputes directly using Swan's APIs or dash To respond to disputes, ask your merchant to [submit a request](https://supportform.swan.io/) to the Swan Support team. +## Known issues {#known-issues} + +:::caution Early access +The following issues have been identified and are not yet resolved. +This section will be removed once they are fixed. +::: + +**`MerchantPayment.Created` webhook not triggered** (issue 1881) + +When a payment succeeds or fails, the `MerchantPayment.Created` webhook is not sent. + +Workaround: use other webhooks to track merchant payments. For successful payments, `MerchantPayment.Authorized` and `MerchantPayment.Captured` are both triggered. For failures, use `MerchantPayment.Rejected`. + +**Gateway timeout on invalid payment amount** (issue 1835) + +Calling `createInPersonPaymentIntent` with an invalid amount (such as an empty string or `"1,5"`) returns a gateway timeout instead of a `ValidationRejection`. + +Workaround: validate the amount value before passing it to the mutation. + +**Incorrect refund balance on rejected payments** (issue 1759) + +When a merchant payment is `Rejected`, the full payment amount appears on the `availableToRefund` balance even though refunds aren't supported for rejected payments. + +Workaround: treat the `availableToRefund` balance as `0` for rejected payments. + ## Guides {#guides} - [Accept in-person card payments](./guide-accept.mdx) From 89ccf92378c4e9cff41b11959399420e03dd4eba Mon Sep 17 00:00:00 2001 From: Maxence Busson Date: Fri, 6 Mar 2026 18:10:46 +0100 Subject: [PATCH 4/4] Fixing final discrepancies caught --- docs/preview/in-person/cards/guide-accept.mdx | 1 + docs/preview/in-person/cards/guide-create-payments.mdx | 10 +++++++++- docs/preview/in-person/cards/guide-initialize.mdx | 2 +- docs/preview/in-person/cards/guide-request-method.mdx | 3 ++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/preview/in-person/cards/guide-accept.mdx b/docs/preview/in-person/cards/guide-accept.mdx index 8e08b7baff..14357f9af0 100644 --- a/docs/preview/in-person/cards/guide-accept.mdx +++ b/docs/preview/in-person/cards/guide-accept.mdx @@ -23,6 +23,7 @@ Before accepting in-person card payments, you need a merchant profile. During this phase, Swan manually sets up merchant profiles with the Stripe connection. After requesting a profile, contact your Product Integration Manager with your merchant profile ID. They will confirm when setup is complete and provide your `$LOCATION_ID`. +In a future release, the location ID will be available as `mainLocationId` on the `InPersonCardMerchantPaymentMethod` object. Swan can configure up to 10 profiles per partner during early access. ::: diff --git a/docs/preview/in-person/cards/guide-create-payments.mdx b/docs/preview/in-person/cards/guide-create-payments.mdx index 3551f8f1db..b870430ed9 100644 --- a/docs/preview/in-person/cards/guide-create-payments.mdx +++ b/docs/preview/in-person/cards/guide-create-payments.mdx @@ -37,7 +37,15 @@ mutation CreateInPersonPayment { __typename message } - ... on AmountTooSmallRejection { + ... on NotFoundRejection { + __typename + message + } + ... on InternalErrorRejection { + __typename + message + } + ... on ValidationRejection { __typename message } diff --git a/docs/preview/in-person/cards/guide-initialize.mdx b/docs/preview/in-person/cards/guide-initialize.mdx index ed38a88d84..9732b8e7a0 100644 --- a/docs/preview/in-person/cards/guide-initialize.mdx +++ b/docs/preview/in-person/cards/guide-initialize.mdx @@ -368,7 +368,7 @@ override fun onStop() { :::caution Location ID: temporary during early access 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. -This value will be available directly through the API in a future release. +In a future release, it will be available as `mainLocationId` on the `InPersonCardMerchantPaymentMethod` object directly from the API. ::: ## Troubleshooting {#troubleshooting} diff --git a/docs/preview/in-person/cards/guide-request-method.mdx b/docs/preview/in-person/cards/guide-request-method.mdx index 37bfea7a5c..3219ed3339 100644 --- a/docs/preview/in-person/cards/guide-request-method.mdx +++ b/docs/preview/in-person/cards/guide-request-method.mdx @@ -31,6 +31,7 @@ mutation RequestInPersonCardMethod { ... on InPersonCardMerchantPaymentMethod { id methodId + mainLocationId statusInfo { status } @@ -44,7 +45,7 @@ mutation RequestInPersonCardMethod { __typename message } - ... on MerchantProfileNotReadyRejection { + ... on InvalidPaymentMethodRequestRejection { __typename message }