AI Integration Prompt - Swift - WalletConnect Pay Docs
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
```
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
```swift
// 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. Collect user data via WebView (if required) │
│ │
│ 5. getRequiredPaymentActions(paymentId, optionId) │
│──────────────────────▶┌─────────────┐ │
│◀──────────────────────│ Pay API │ │
│ Actions[] (walletRpc — method varies) │
│ └─────────────┘ │
│ │
│ 6. Sign typed data (wallet's EIP-712 signer) │
│ │
│ 7. confirmPayment(signatures) │
│──────────────────────▶┌─────────────┐ ┌────────────┐ │
│ │ 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
)
}
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 URIspay_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. 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.
// The WebView submits data directly to the backend.
try await showDataCollectionWebView(url: url)
}
// 2. Get required signing actions
let actions = try await WalletKit.instance.Pay.getRequiredPaymentActions(
paymentId: paymentId,
optionId: selectedOption.id
)
// 3. 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 - e.g., "eth_signTypedData_v4", "eth_sendTransaction"
// rpc.params - JSON-encoded parameters
guard let paramsData = rpc.params.data(using: .utf8),
let params = try JSONSerialization.jsonObject(with: paramsData) as? [Any] else {
throw PaymentError.invalidParams
}
switch rpc.method {
case "eth_signTypedData_v4":
guard params.count >= 2,
let typedDataJson = params[1] as? String else {
throw PaymentError.invalidParams
}
let signature = try await yourWallet.signTypedData(typedDataJson)
signatures.append(signature)
case "eth_sendTransaction":
guard let txParams = params[0] as? [String: Any] else {
throw PaymentError.invalidParams
}
let txHash = try await yourWallet.sendTransaction(txParams)
signatures.append(txHash)
case "personal_sign":
guard params.count >= 2,
let message = params[0] as? String else {
throw PaymentError.invalidParams
}
let signature = try await yourWallet.personalSign(message)
signatures.append(signature)
default:
throw PaymentError.unsupportedMethod(rpc.method)
}
}
// 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 selectedOption.collectData.url is present, display the URL in a WKWebView. The hosted form handles rendering, validation, and T&C acceptance. The form URL accepts optional query parameters — append them to selectedOption.collectData.url before loading, preserving existing query parameters:
prefill— base64url-encoded JSON of known user fields. Keys must match therequiredfields fromcollectData.schema(e.g.fullName,dob,pobAddress).theme—lightordark, to match your wallet’s color mode.themeVariables— a base64url string exported from the WalletConnect Pay Dashboard that overrides design tokens (font, font size, some colors, button/input border radius). Append it verbatim.
func buildFormUrl(
baseUrl: String,
prefillData: [String: String] = [:],
theme: String? = nil, // "light" or "dark"
themeVariables: String? = nil // base64url string exported from the Pay Dashboard
) -> String {
guard var components = URLComponents(string: baseUrl) else { return baseUrl }
var queryItems = components.queryItems ?? []
if !prefillData.isEmpty,
let jsonData = try? JSONSerialization.data(withJSONObject: prefillData) {
// base64url: URL-safe, no padding
let prefill = jsonData.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
queryItems.append(URLQueryItem(name: "prefill", value: prefill))
}
if let theme = theme { queryItems.append(URLQueryItem(name: "theme", value: theme)) }
if let themeVariables = themeVariables {
queryItems.append(URLQueryItem(name: "themeVariables", value: themeVariables))
}
components.queryItems = queryItems
return components.string ?? baseUrl
}
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
collectedDatatoconfirmPayment(). 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 // e.g., "eth_signTypedData_v4", "eth_sendTransaction"
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 | Check action.walletRpc.method and dispatch to the correct handler for each method |
| 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?.urlafter the user selects an option and show the WebView before callinggetRequiredPaymentActions. - Not handling cold start - Deep links on app launch need delayed handling.
- Not dispatching on RPC method — Always check
action.walletRpc.methodand use the appropriate handler. For example,eth_signTypedData_v4requires EIP-712 signing, whileeth_sendTransactionrequires sending a transaction on-chain. - 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
- POS:
- Test Cold vs Warm Start: - Cold: Kill app, open deep link
- Warm: App in background, open deep link