diff --git a/docs/preview/in-person/cards/guide-accept.mdx b/docs/preview/in-person/cards/guide-accept.mdx
index 2b8c1b73bc..14357f9af0 100644
--- a/docs/preview/in-person/cards/guide-accept.mdx
+++ b/docs/preview/in-person/cards/guide-accept.mdx
@@ -9,28 +9,45 @@ 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)
+:::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`.
+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.
+:::
-You can also update merchant profiles if needed.
+→ [Request a merchant profile](../../../topics/merchants/profiles/guide-request.mdx)
-→ [Update a merchant profile](../../../topics/merchants/profiles/guide-update.mdx)
+## Step 3: Request the payment method {#step-3}
-## Step 2: Request the payment method {#step-2}
+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 3: Create in-person card payments {#step-3}
+## Step 4: Initialize the device {#step-4}
+
+Set up the connection token provider and connect your device to the Stripe Terminal reader.
+
+→ [Initialize the device for payments](./guide-initialize.mdx)
+
+## 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..b870430ed9 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.
@@ -63,7 +37,15 @@ mutation CreateInPersonPayment {
__typename
message
}
- ... on AmountTooSmallRejection {
+ ... on NotFoundRejection {
+ __typename
+ message
+ }
+ ... on InternalErrorRejection {
+ __typename
+ message
+ }
+ ... on ValidationRejection {
__typename
message
}
@@ -71,50 +53,201 @@ 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.
-:::
+### 2.1: Retrieve the payment intent {#retrieve-intent}
+
+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;
+}
+```
-### 3.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.
+```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
+ }
+ }
+)
+```
-### 3.2: Collect the payment method {#collect-method}
+
+
-Call the Stripe SDK's `collectPaymentMethod` method.
+### 2.2: Collect the payment method {#collect-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.
-### 3.3: Confirm the payment intent {#confirm-intent}
+
+
+
+```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 `confirmPaymentIntent` to finalize the payment.
+The payment is authorized and captured automatically.
-Call the Stripe SDK's `confirmPaymentIntent` method to finalize the payment.
-The payment is 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}
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. |
@@ -122,7 +255,7 @@ These are the key statuses after a payment is processed:
## 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
new file mode 100644
index 0000000000..9732b8e7a0
--- /dev/null
+++ b/docs/preview/in-person/cards/guide-initialize.mdx
@@ -0,0 +1,404 @@
+---
+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.
+
+### 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 RequestTerminalConnectionTokenSuccessPayload {
+ connectionToken
+ }
+ ... on ForbiddenRejection {
+ __typename
+ message
+ }
+ ... on InternalErrorRejection {
+ __typename
+ message
+ }
+ ... on NotFoundRejection {
+ __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")
+ // 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.
+
+```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",
+ // 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) {}
+ })
+}
+```
+
+
+
+
+:::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.
+In a future release, it will be available as `mainLocationId` on the `InPersonCardMerchantPaymentMethod` object directly from the API.
+:::
+
+## 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/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
}
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..7f50a7d559
--- /dev/null
+++ b/docs/preview/in-person/cards/guide-setup.mdx
@@ -0,0 +1,234 @@
+---
+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 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/).
+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
+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.
+:::
+
+## 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 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.
+:::
+
+
+
+
+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 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.
+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..fb43235cd9 100644
--- a/docs/preview/in-person/cards/index.mdx
+++ b/docs/preview/in-person/cards/index.mdx
@@ -150,10 +150,37 @@ 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 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",
],
},