Documentation Index
Fetch the complete documentation index at: https://walletconnect-pay-docs-tomiir-buyer-checkout-docs.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
WalletConnect Pay Integration via WalletKit (Swift)
Integration guide for iOS/Swift wallets that already have WalletKit integrated.
Agent Guidance (For AI Assistants)
Adaptation Principles
-
WalletKit Required - This guide assumes WalletKit is already configured. Pay is accessed via
WalletKit.instance.Pay.*.
-
Configuration Order Matters:
Networking.configure(projectId:) // FIRST
WalletKit.configure(...) // SECOND - Pay auto-configured
-
CAIP-10 Format Required - All accounts must be in
eip155:{chainId}:{address} format.
-
Signature Order Critical - Signatures array must match actions array order exactly.
-
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:
- Replace Signing Logic - Use the wallet’s native EIP-712 signing implementation.
- Adapt UI Flow - Match the wallet’s existing design patterns and navigation.
- Account Discovery - Use the wallet’s account management to build CAIP-10 accounts.
- 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
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
)
}
Step 2: Handle Deep Links
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)
}
}
Step 3: Detect Payment Links
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
| Symptom | Cause | Solution |
|---|
| ”You must call configure() before accessing instance” | WalletKit not configured | Call Networking.configure() then WalletKit.configure() at app launch |
| Pay methods not available | Wrong import | Use import ReownWalletKit |
Deep Link Issues
| Symptom | Cause | Solution |
|---|
| App not opening from links | URL scheme missing | Add CFBundleURLSchemes to Info.plist |
| Payment link not detected | Format not recognized | Use WalletKit.isPaymentLink() |
| Cold start link ignored | UI not ready | Add delay before handling |
Payment Failures
| Error | Meaning | Solution |
|---|
paymentExpired | Link expired | Merchant generates new link |
routeExpired | Route expired | Retry getPaymentOptions |
invalidSignature | Signature mismatch | Check signature order matches actions |
optionNotFound | Stale option | Refresh options and reselect |
Signing Issues
| Symptom | Cause | Solution |
|---|
| Invalid signature error | Wrong method | Use eth_signTypedData_v4 (EIP-712) |
| Params parsing fails | Unexpected format | Parse as ["address", "typedDataJson"] |
File Checklist
| File | Purpose |
|---|
Info.plist | URL scheme configuration |
SceneDelegate.swift | Deep link handling |
PaymentPresenter.swift | Payment flow logic |
PaymentView.swift | Payment UI |
Common Pitfalls
-
Forgetting CAIP-10 format - Accounts must be
eip155:{chainId}:{address}, not just addresses.
-
Wrong signature order - Signatures array must match actions array order exactly.
-
Skipping WebView data collection - Always check
selectedOption.collectData?.url after the user selects an option and show the WebView before confirmation.
-
Not handling cold start - Deep links on app launch need delayed handling.
-
Using wrong signing method - Must use
eth_signTypedData_v4 for EIP-712 typed data.
-
Missing chains - Provide accounts for all chains you support to maximize payment options.
Testing
-
Enable Logging:
WalletKit.configure(..., payLogging: true)
-
Test Both Deep Link Formats:
- WC URI:
yourwallet://?uri=wc:abc...&pay=pay_xyz
- POS:
yourwallet://walletconnectpay?paymentId=pay_xyz
-
Test Cold vs Warm Start:
- Cold: Kill app, open deep link
- Warm: App in background, open deep link
Support