Skip to main content

WalletConnect Pay Integration via WalletKit (Swift)

Integration guide for iOS/Swift wallets that already have WalletKit integrated.

Agent Guidance (For AI Assistants)

Adaptation Principles

  1. WalletKit Required - This guide assumes WalletKit is already configured. Pay is accessed via WalletKit.instance.Pay.*.
  2. Configuration Order Matters:
    Networking.configure(projectId:)  // FIRST
    WalletKit.configure(...)          // SECOND - Pay auto-configured
    
  3. CAIP-10 Format Required - All accounts must be in eip155:{chainId}:{address} format.
  4. Signature Order Critical - Signatures array must match actions array order exactly.
  5. Travel Rule Conditional - Only collect user data when selectedOption.collectData is non-nil. IC is per-option, not global.

Code Adaptation Guidelines

When adapting for a specific wallet:
  1. Replace Signing Logic - Use the wallet’s native EIP-712 signing implementation.
  2. Adapt UI Flow - Match the wallet’s existing design patterns and navigation.
  3. Account Discovery - Use the wallet’s account management to build CAIP-10 accounts.
  4. Error Handling - Add wallet-specific error handling and user feedback.

Common Patterns

// Pattern: Check for payment link in QR scanner
func handleScannedQR(_ content: String) {
    if WalletKit.isPaymentLink(content) {
        startPaymentFlow(paymentLink: content)
    } else if content.hasPrefix("wc:") {
        startPairingFlow(uri: content)
    }
}

// Pattern: Multi-chain accounts
func getCAIP10Accounts(address: String) -> [String] {
    return [
        "eip155:1:\(address)",      // Ethereum
        "eip155:137:\(address)",    // Polygon
        "eip155:8453:\(address)",   // Base
        "eip155:42161:\(address)"   // Arbitrum
    ]
}

Prerequisites

  • iOS 13.0+
  • Swift 5.7+
  • WalletKit already integrated
  • WalletConnect Cloud PROJECT_ID (Get one)
// Package.swift
dependencies: [
    .package(url: "https://github.com/reown-com/reown-swift", from: "1.0.0")
]

Architecture Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                              PAYMENT FLOW                                    │
└─────────────────────────────────────────────────────────────────────────────┘

  ┌──────────┐                                                    ┌──────────┐
  │  User    │                                                    │ Merchant │
  │  Wallet  │                                                    │   POS    │
  └────┬─────┘                                                    └────┬─────┘
       │                                                               │
       │  1. Scan QR / Open Deep Link                                  │
       │◀──────────────────────────────────────────────────────────────┤
       │                                                               │
       │  2. getPaymentOptions(paymentLink, accounts)                  │
       │──────────────────────▶┌─────────────┐                         │
       │                       │  Pay API    │                         │
       │◀──────────────────────└─────────────┘                         │
       │  PaymentOptionsResponse                                       │
       │                                                               │
       │  3. User selects payment option                               │
       │                                                               │
       │  4. getRequiredPaymentActions(paymentId, optionId)            │
       │──────────────────────▶┌─────────────┐                         │
       │◀──────────────────────│  Pay API    │                         │
       │  Actions[] (eth_signTypedData_v4)                             │
       │                       └─────────────┘                         │
       │                                                               │
       │  5. Sign typed data (wallet's EIP-712 signer)                 │
       │                                                               │
       │  6. Collect user data (if travel rule required)               │
       │                                                               │
       │  7. confirmPayment(signatures, collectedData)                 │
       │──────────────────────▶┌─────────────┐     ┌────────────┐      │
       │                       │  Pay API    │────▶│ Blockchain │      │
       │◀──────────────────────└─────────────┘     └────────────┘      │
       │  ConfirmPaymentResultResponse (status: succeeded)             │
       │                                                               │
       ▼                                                               ▼
  ┌──────────┐                                                    ┌──────────┐
  │ Payment  │                                                    │ Payment  │
  │ Complete │                                                    │ Received │
  └──────────┘                                                    └──────────┘

State Machine

[Options] ──▶ [WebView Data Collection]* ──▶ [Confirmation] ──▶ [Confirming] ──▶ [Success]


                                                  [Error]

* WebView step - skip if selectedOption.collectData is nil

Step 1: Configure WalletKit

IMPORTANT: Pay is auto-configured when you configure WalletKit. No separate Pay configuration needed.
import ReownWalletKit

func configureWalletKit() {
    // 1. Configure Networking with your project ID
    Networking.configure(
        groupIdentifier: "group.com.yourcompany.wallet",
        projectId: "YOUR_PROJECT_ID",
        socketFactory: DefaultSocketFactory()
    )

    // 2. Configure WalletKit - Pay is automatically configured
    let metadata = AppMetadata(
        name: "Your Wallet",
        description: "A crypto wallet",
        url: "https://yourwallet.com",
        icons: ["https://yourwallet.com/icon.png"],
        redirect: try! AppMetadata.Redirect(
            native: "yourwallet://",
            universal: "https://yourwallet.com/wc",
            linkMode: true
        )
    )

    WalletKit.configure(
        metadata: metadata,
        crypto: DefaultCryptoProvider(),
        environment: .production,
        payLogging: true  // Enable for debugging
    )
}

Info.plist Configuration

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>yourwallet</string>
        </array>
    </dict>
</array>

SceneDelegate Implementation

import UIKit
import ReownWalletKit

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    // MARK: - Cold Start

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        // ... window setup ...

        // Check for payment deep link on cold start
        if let urlContext = connectionOptions.urlContexts.first {
            if let paymentLink = extractPaymentLink(from: urlContext.url) {
                // Delay to ensure UI is ready
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
                    self?.handlePaymentLink(paymentLink)
                }
            }
        }
    }

    // MARK: - Warm Start

    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        guard let url = URLContexts.first?.url else { return }

        if let paymentLink = extractPaymentLink(from: url) {
            handlePaymentLink(paymentLink)

            // For POS scan format, return early - no pairing needed
            if url.host == "walletconnectpay" {
                return
            }
        }

        // Continue with regular WalletConnect pairing if needed...
    }

    // MARK: - Payment Link Extraction

    /// Supports two formats:
    /// 1. WC URI format: yourwallet://?uri=wc:abc...&pay=pay_xyz
    /// 2. POS scan format: yourwallet://walletconnectpay?paymentId=pay_xyz
    private func extractPaymentLink(from url: URL) -> String? {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
              let queryItems = components.queryItems else {
            return nil
        }

        // Format 1: WC URI with embedded pay param
        if let uriValue = queryItems.first(where: { $0.name == "uri" })?.value,
           WalletKit.isPaymentLink(uriValue) {
            return uriValue
        }

        // Format 2: POS scan format
        if url.host == "walletconnectpay",
           let paymentId = queryItems.first(where: { $0.name == "paymentId" })?.value {
            return "yourwallet://walletconnectpay?paymentId=\(paymentId)"
        }

        return nil
    }

    private func handlePaymentLink(_ paymentLink: String) {
        // Get wallet address and build CAIP-10 accounts
        let address = getWalletAddress()
        let accounts = [
            "eip155:1:\(address)",
            "eip155:137:\(address)",
            "eip155:8453:\(address)"
        ]

        // Present your payment UI
        presentPaymentFlow(paymentLink: paymentLink, accounts: accounts)
    }
}

Use WalletKit.isPaymentLink() to detect payment links from QR codes or deep links.
// Static method - can be called BEFORE WalletKit.configure()
if WalletKit.isPaymentLink(scannedString) {
    handlePaymentFlow(paymentLink: scannedString)
}

// Instance method - after configure()
if WalletKit.instance.Pay.isPaymentLink(scannedString) {
    handlePaymentFlow(paymentLink: scannedString)
}
Detection Patterns:
  • pay. hosts (e.g., pay.walletconnect.com)
  • pay= parameter in WalletConnect URIs
  • pay_ prefix in bare payment IDs

Step 4: Get Payment Options

func loadPaymentOptions(paymentLink: String, walletAddress: String) async throws -> PaymentOptionsResponse {
    // Build CAIP-10 accounts for supported chains
    let accounts = [
        "eip155:1:\(walletAddress)",      // Ethereum
        "eip155:137:\(walletAddress)",    // Polygon
        "eip155:8453:\(walletAddress)",   // Base
        "eip155:42161:\(walletAddress)"   // Arbitrum
    ]

    let response = try await WalletKit.instance.Pay.getPaymentOptions(
        paymentLink: paymentLink,
        accounts: accounts,
        includePaymentInfo: true
    )

    // Display merchant info
    if let info = response.info {
        print("Merchant: \(info.merchant.name)")
        print("Amount: \(info.amount.display.assetSymbol) \(info.amount.value)")
    }

    // Check which options require data collection
    for option in response.options {
        if option.collectData != nil {
            print("Option \(option.id) requires info capture")
        }
    }

    return response
}
CAIP-10 Format: eip155:{chainId}:{address}

Step 5: Complete Payment Flow

func confirmPayment(
    paymentId: String,
    selectedOption: PaymentOption,
    response: PaymentOptionsResponse
) async throws {

    // 1. Get required signing actions
    let actions = try await WalletKit.instance.Pay.getRequiredPaymentActions(
        paymentId: paymentId,
        optionId: selectedOption.id
    )

    // 2. Sign each action using YOUR wallet's signer
    var signatures: [String] = []
    for action in actions {
        let rpc = action.walletRpc

        // rpc.chainId  - e.g., "eip155:8453"
        // rpc.method   - "eth_signTypedData_v4"
        // rpc.params   - JSON: ["address", "typedDataJson"]

        // Parse typed data from params
        guard let paramsData = rpc.params.data(using: .utf8),
              let params = try JSONSerialization.jsonObject(with: paramsData) as? [Any],
              params.count >= 2,
              let typedDataJson = params[1] as? String else {
            throw PaymentError.invalidParams
        }

        // Sign using your wallet's EIP-712 implementation
        let signature = try await yourWallet.signTypedData(typedDataJson)
        signatures.append(signature)
    }

    // 3. Collect data via WebView if required for selected option
    if let collectData = selectedOption.collectData, let url = collectData.url {
        // Show WebView and wait for IC_COMPLETE message
        try await showDataCollectionWebView(url: url)
    }

    // 4. Confirm payment
    let result = try await WalletKit.instance.Pay.confirmPayment(
        paymentId: paymentId,
        optionId: selectedOption.id,
        signatures: signatures,
        maxPollMs: 60000
    )

    switch result.status {
    case .succeeded:
        print("Payment successful!")
    case .processing:
        print("Payment processing...")
    case .failed:
        print("Payment failed")
    case .expired:
        print("Payment expired")
    case .requiresAction:
        print("Additional action required")
    }
}
CRITICAL: Signatures must be in the same order as the actions array.

WebView Data Collection

When collectData.url is present, display the URL in a WKWebView. The hosted form handles rendering, validation, and T&C acceptance.
import WebKit
import SwiftUI

struct PayDataCollectionWebView: UIViewRepresentable {
    let url: URL
    let onComplete: () -> Void
    let onError: (String) -> Void

    func makeCoordinator() -> Coordinator {
        Coordinator(onComplete: onComplete, onError: onError)
    }

    func makeUIView(context: Context) -> WKWebView {
        let config = WKWebViewConfiguration()
        config.userContentController.add(
            context.coordinator,
            name: "payDataCollectionComplete"
        )

        let webView = WKWebView(frame: .zero, configuration: config)
        webView.navigationDelegate = context.coordinator
        webView.load(URLRequest(url: url))
        return webView
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {}

    class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
        let onComplete: () -> Void
        let onError: (String) -> Void

        init(onComplete: @escaping () -> Void, onError: @escaping (String) -> Void) {
            self.onComplete = onComplete
            self.onError = onError
        }

        func userContentController(
            _ userContentController: WKUserContentController,
            didReceive message: WKScriptMessage
        ) {
            guard let body = message.body as? String,
                  let data = body.data(using: .utf8),
                  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
                  let type = json["type"] as? String else { return }

            DispatchQueue.main.async {
                switch type {
                case "IC_COMPLETE":
                    self.onComplete()
                case "IC_ERROR":
                    let error = json["error"] as? String ?? "Unknown error"
                    self.onError(error)
                default:
                    break
                }
            }
        }

        func webView(
            _ webView: WKWebView,
            decidePolicyFor navigationAction: WKNavigationAction,
            decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
        ) {
            guard let url = navigationAction.request.url else {
                decisionHandler(.allow)
                return
            }
            if let host = url.host, !host.contains("pay.walletconnect.com") {
                UIApplication.shared.open(url)
                decisionHandler(.cancel)
                return
            }
            decisionHandler(.allow)
        }
    }
}
Important: When using the WebView approach, do not pass collectedData to confirmPayment(). The WebView submits data directly to the backend.

Core API Reference

Methods

// Check if string is a payment link (static - works before configure)
WalletKit.isPaymentLink(_ string: String) -> Bool

// Get available payment options
WalletKit.instance.Pay.getPaymentOptions(
    paymentLink: String,
    accounts: [String],           // CAIP-10 format
    includePaymentInfo: Bool = true
) async throws -> PaymentOptionsResponse

// Get signing actions for selected option
WalletKit.instance.Pay.getRequiredPaymentActions(
    paymentId: String,
    optionId: String
) async throws -> [Action]

// Confirm payment with signatures
WalletKit.instance.Pay.confirmPayment(
    paymentId: String,
    optionId: String,
    signatures: [String],
    maxPollMs: Int64? = nil
) async throws -> ConfirmPaymentResultResponse

Types

struct PaymentOptionsResponse {
    let paymentId: String
    let info: PaymentInfo?
    let options: [PaymentOption]
    let collectData: CollectDataAction?    // nil if no travel rule
    let resultInfo: PaymentResultInfo?     // present when payment already completed
}

struct PaymentResultInfo {
    let txId: String
    let optionAmount: PayAmount
}

struct PaymentInfo {
    let status: PaymentStatus
    let amount: PayAmount
    let expiresAt: Int64
    let merchant: MerchantInfo
}

struct PaymentOption {
    let id: String
    let amount: PayAmount
    let etaS: Int64
    let collectData: CollectDataAction?  // Per-option data collection (nil if not required)
}

struct PayAmount {
    let unit: String
    let value: String
    let display: AmountDisplay
}

struct AmountDisplay {
    let assetSymbol: String
    let assetName: String
    let decimals: Int64
    let iconUrl: String?
    let networkName: String?
}

struct Action {
    let walletRpc: WalletRpcAction
}

struct WalletRpcAction {
    let chainId: String    // "eip155:8453"
    let method: String     // "eth_signTypedData_v4"
    let params: String     // JSON: ["address", "typedDataJson"]
}

struct CollectDataAction {
    let url: String                    // WebView URL for data collection
    let schema: String?                // JSON schema describing required fields
}

struct ConfirmPaymentResultResponse {
    let status: PaymentStatus
    let isFinal: Bool
}

enum PaymentStatus {
    case requiresAction
    case processing
    case succeeded
    case failed
    case expired
}

Troubleshooting

Configuration Issues

SymptomCauseSolution
”You must call configure() before accessing instance”WalletKit not configuredCall Networking.configure() then WalletKit.configure() at app launch
Pay methods not availableWrong importUse import ReownWalletKit
SymptomCauseSolution
App not opening from linksURL scheme missingAdd CFBundleURLSchemes to Info.plist
Payment link not detectedFormat not recognizedUse WalletKit.isPaymentLink()
Cold start link ignoredUI not readyAdd delay before handling

Payment Failures

ErrorMeaningSolution
paymentExpiredLink expiredMerchant generates new link
routeExpiredRoute expiredRetry getPaymentOptions
invalidSignatureSignature mismatchCheck signature order matches actions
optionNotFoundStale optionRefresh options and reselect

Signing Issues

SymptomCauseSolution
Invalid signature errorWrong methodUse eth_signTypedData_v4 (EIP-712)
Params parsing failsUnexpected formatParse as ["address", "typedDataJson"]

File Checklist

FilePurpose
Info.plistURL scheme configuration
SceneDelegate.swiftDeep link handling
PaymentPresenter.swiftPayment flow logic
PaymentView.swiftPayment UI

Common Pitfalls

  1. Forgetting CAIP-10 format - Accounts must be eip155:{chainId}:{address}, not just addresses.
  2. Wrong signature order - Signatures array must match actions array order exactly.
  3. Skipping WebView data collection - Always check selectedOption.collectData?.url after the user selects an option and show the WebView before confirmation.
  4. Not handling cold start - Deep links on app launch need delayed handling.
  5. Using wrong signing method - Must use eth_signTypedData_v4 for EIP-712 typed data.
  6. Missing chains - Provide accounts for all chains you support to maximize payment options.

Testing

  1. Enable Logging:
    WalletKit.configure(..., payLogging: true)
    
  2. Test Both Deep Link Formats:
    • WC URI: yourwallet://?uri=wc:abc...&pay=pay_xyz
    • POS: yourwallet://walletconnectpay?paymentId=pay_xyz
  3. Test Cold vs Warm Start:
    • Cold: Kill app, open deep link
    • Warm: App in background, open deep link

Support