From b8c6b448d4b5e3d9dc95c8400b72afcae1574f04 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 07:43:08 +0000 Subject: [PATCH 1/2] docs: add Tap to Pay wallet integration guide Co-Authored-By: mirna@reown.com --- docs.json | 6 +- payments/wallets/tap-to-pay.mdx | 600 ++++++++++++++++++++++++++++++++ 2 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 payments/wallets/tap-to-pay.mdx diff --git a/docs.json b/docs.json index bb1cc4a..5bc2654 100644 --- a/docs.json +++ b/docs.json @@ -68,7 +68,8 @@ ] }, "payments/wallets/api-first", - "payments/wallets/usdt-support" + "payments/wallets/usdt-support", + "payments/wallets/tap-to-pay" ] }, { @@ -191,7 +192,8 @@ ] }, "payments/wallets/api-first", - "payments/wallets/usdt-support" + "payments/wallets/usdt-support", + "payments/wallets/tap-to-pay" ] }, { diff --git a/payments/wallets/tap-to-pay.mdx b/payments/wallets/tap-to-pay.mdx new file mode 100644 index 0000000..da0391a --- /dev/null +++ b/payments/wallets/tap-to-pay.mdx @@ -0,0 +1,600 @@ +--- +title: "Tap to Pay - Wallet Integration Guide" +description: "Integrate NFC Tap to Pay into your wallet app on Android and iOS to enable contactless payments at POS terminals using WalletConnect Pay." +sidebarTitle: "Tap to Pay" +--- + + +Tap to Pay is an experimental feature. If you're interested in implementing and testing it in your wallet, please [contact us](mailto:support@reown.com) before getting started. + + +This guide covers how to integrate Tap to Pay into a third-party wallet app on Android and iOS. + +## Android + +### How It Works + +When a user taps their phone on a POS terminal, the terminal emits an NDEF tag with a single URI record pointing to `https://pay.walletconnect.com`. Android dispatches this in one of three ways: + +- **One wallet installed with a verified App Link** — Android opens the wallet's `NfcPaymentActivity` directly. +- **Multiple wallets installed** — Android shows the "Open with..." chooser; user selects a wallet. +- **No wallet installed** — Android opens the URL in the browser, which shows a wallet chooser page with App Store / Play Store links. + +When the wallet app is already in the foreground, foreground dispatch takes priority over manifest filters and delivers the intent directly. + +#### Flow: Tap from Home Screen (Phone Unlocked, No App in Foreground) + +```mermaid +sequenceDiagram + participant POS as POS Terminal + participant NFC as Android NFC Stack + participant OS as Android OS + participant W as Wallet App + participant B as Browser + + POS->>NFC: NDEF tag emulated (single URI record) + NFC->>OS: NDEF_DISCOVERED intent
(matches URI: https://pay.walletconnect.com) + + alt 1 wallet with verified App Link + OS->>W: App Link opens NfcPaymentActivity + W->>W: Extract payment URL from intent data + W->>W: Start payment flow + else Multiple wallets installed + OS->>OS: Show Activity Chooser
("Open with...") + OS->>W: User selects wallet + W->>W: Extract payment URL from intent data + W->>W: Start payment flow + else No wallet installed + OS->>B: Open https://pay.walletconnect.com/?pid=pay_xxx + B->>B: Wallet chooser web page
(App Store / Play Store links) + end +``` + +#### Flow: Tap from Wallet App (Foreground) + +```mermaid +sequenceDiagram + participant POS as POS Terminal + participant NFC as Android NFC + participant W as Wallet App (Foreground) + + Note over W: App is open, NFC foreground dispatch active + + POS->>NFC: NDEF tag emulated (URI record) + NFC->>W: NDEF_DISCOVERED dispatched
to foreground Activity + W->>W: Parse URI record + W->>W: Extract payment URL + W->>W: Start payment flow immediately +``` + +### Integration Steps + + + + Android App Links require `pay.walletconnect.com` to list your app in its `/.well-known/assetlinks.json` file. Without this, `autoVerify` intent filters won't pass verification. + + Provide the following to WalletConnect: + - Package name + - SHA-256 certificate fingerprint(s) + + + + Add NFC permissions: + + ```xml AndroidManifest.xml + + + + ``` + + Register a translucent activity with three intent filters — each covers a different Android dispatch path for the same NFC tap: + + ```xml AndroidManifest.xml + + + + + + + + + + + + + + + + + + + + + + + + + + ``` + + Create `res/xml/nfc_tech_filter.xml`: + + ```xml nfc_tech_filter.xml + + + android.nfc.tech.IsoDep + android.nfc.tech.Ndef + + + android.nfc.tech.Ndef + + + android.nfc.tech.NfcA + + + android.nfc.tech.NfcB + + + ``` + + + + The activity handles multiple intent paths to extract the payment URL: + + ```kotlin NfcPaymentActivity.kt + class NfcPaymentActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val paymentUrl = extractPaymentUrl(intent) + if (paymentUrl != null) openPaymentFlow(paymentUrl) + finish() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + val paymentUrl = extractPaymentUrl(intent) ?: return + openPaymentFlow(paymentUrl) + finish() + } + + private fun openPaymentFlow(paymentUrl: String) { + startActivity( + Intent(this, YourMainWalletActivity::class.java).apply { + action = ACTION_NFC_PAYMENT + putExtra(EXTRA_PAYMENT_URL, paymentUrl) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + ) + } + + private fun extractPaymentUrl(intent: Intent?): String? { + if (intent == null) return null + return when (intent.action) { + // App Links — URL is in intent.data + Intent.ACTION_VIEW -> + intent.data?.let { unwrapPaymentUrl(it.toString()) } + // NDEF tag — URL is in NDEF extras + NfcAdapter.ACTION_NDEF_DISCOVERED -> + extractFromNdefExtras(intent) + // TECH fallback — read NDEF from Tag object + NfcAdapter.ACTION_TECH_DISCOVERED -> + extractFromTag(intent) ?: extractFromNdefExtras(intent) + else -> null + } + } + } + ``` + + + + When the wallet activity is in the foreground, manifest intent filters may not fire. Use foreground dispatch to claim NFC priority: + + ```kotlin NfcPaymentReader.kt + class NfcPaymentReader( + private val activity: Activity, + private val onPaymentUrl: (String) -> Unit, + ) { + private val nfcAdapter = NfcAdapter.getDefaultAdapter(activity) + + private val pendingIntent by lazy { + PendingIntent.getActivity( + activity, 0, + Intent(activity, activity.javaClass) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), + PendingIntent.FLAG_MUTABLE + ) + } + + fun enable() { + nfcAdapter?.enableForegroundDispatch( + activity, pendingIntent, + arrayOf(IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)), + arrayOf( + arrayOf(Ndef::class.java.name), + arrayOf(IsoDep::class.java.name) + ) + ) + } + + fun disable() { + nfcAdapter?.disableForegroundDispatch(activity) + } + + /** Call from Activity.onNewIntent(). Returns true if handled. */ + fun handleIntent(intent: Intent): Boolean { + // Same NDEF/Tag extraction logic as NfcPaymentActivity. + // If a pay.walletconnect.com URL is found, call onPaymentUrl(url) and return true. + // Otherwise return false. + } + } + ``` + + Wire it into your main activity: + + ```kotlin MainActivity.kt + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + nfcReader = NfcPaymentReader(this) { url -> processPayment(url) } + } + + override fun onResume() { super.onResume(); nfcReader.enable() } + override fun onPause() { super.onPause(); nfcReader.disable() } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + if (nfcReader.handleIntent(intent)) return + // ...other intent handling + } + ``` + + + + ```kotlin PaymentFlow.kt + // 1. Fetch payment options + val accounts = listOf("eip155:1:0xABC...", "eip155:137:0xABC...") + val options = WalletKit.Pay.getPaymentOptions(paymentUrl, accounts).getOrThrow() + + // 2. Display options to user; user selects one + + // 3. Get required signing actions + val actions = WalletKit.Pay.getRequiredPaymentActions(paymentId, optionId).getOrThrow() + + // 4. Sign each action + val signatures = actions + .filterIsInstance() + .map { rpc -> yourSigner.sign(rpc.action.method, rpc.action.params) } + + // 5. Confirm payment + WalletKit.Pay.confirmPayment( + Wallet.Params.ConfirmPayment(paymentId, optionId, signatures, collectedData) + ) + ``` + + + +## iOS + +### How It Works + +When a user taps their phone on a POS terminal, iOS reads the NDEF tag automatically (Background Tag Reading, iOS 13+) and resolves the URL as a Universal Link. Depending on what's installed: + +- **One wallet registered in AASA** — iOS shows a notification banner "Open in [Wallet]"; user taps to open. +- **Multiple wallets in AASA** — AASA is evaluated top-to-bottom; first installed match wins (no disambiguation UI). On iOS 26, a system wallet chooser modal is shown instead. +- **No wallet installed** — iOS opens the URL in Safari, which shows a wallet chooser page with App Store links. + +When the wallet app is in the foreground, the app can trigger a manual NFC scan via `NFCNDEFReaderSession`, which takes priority over Background Tag Reading. + +#### Flow: Tap from Home Screen (Phone Unlocked, No App in Foreground) + +```mermaid +sequenceDiagram + participant POS as POS Terminal + participant NFC as iOS Background Tag Reading + participant OS as iOS + participant W as Wallet App + participant S as Safari + + POS->>NFC: NDEF tag detected (screen on) + NFC->>OS: Parse NDEF records
Find URI record with Universal Link + + alt 1 wallet registered in AASA + OS->>OS: Show notification banner:
"Open in [Wallet]" + OS->>W: User taps notification + W->>W: SceneDelegate receives Universal Link + W->>W: Start payment flow (autoPayMode) + else Multiple wallets in AASA + OS->>OS: AASA evaluated top-to-bottom + OS->>W: First installed match wins
(no disambiguation UI) + W->>W: SceneDelegate receives Universal Link + W->>W: Start payment flow (autoPayMode) + else Multiple wallets in AASA (iOS 26) + OS->>OS: Show system wallet chooser modal + OS->>W: User picks a wallet + W->>W: SceneDelegate receives Universal Link + W->>W: Start payment flow (autoPayMode) + else No wallet installed + OS->>S: Open payment URL in Safari + S->>S: Wallet chooser web page
(App Store links) + end +``` + +#### Flow: Tap from Wallet App (Foreground) + +```mermaid +sequenceDiagram + participant POS as POS Terminal + participant W as Wallet App (Foreground) + participant NFC as NFCNDEFReaderSession + participant Pay as Payment Flow + + W->>W: Press NFC scanner button + W->>NFC: onAppear() -> NFCPaymentReader.scan() + NFC->>NFC: Show "Ready to Pay" NFC sheet + Note over NFC: Active reader session takes
priority over Background Tag Reading + + POS->>NFC: NDEF tag detected + NFC->>NFC: invalidateAfterFirstRead: true + NFC->>W: didDetectNDEFs callback + W->>W: Extract URI from NDEF record + NFC->>NFC: Auto-close NFC sheet + + W->>Pay: Present PayModule (full screen) + Pay->>Pay: Payment confirmation + signing + + Note over Pay: Payment completes + Pay->>W: Dismiss PayModule + W->>W: Return to Balances screen + W->>NFC: onAppear() restarts NFC session + NFC->>NFC: Show "Ready to Pay" again +``` + +### Integration Steps + + + + Universal Links require `pay.walletconnect.com` to host an Apple App Site Association (AASA) file listing your app. WalletConnect will add your app to `pay.walletconnect.com/.well-known/apple-app-site-association`. + + Provide: + - Apple Team ID + - Bundle ID + + + + Add two entitlements in your `.entitlements` file. + + **Associated Domains** (for Universal Links / Background Tag Reading): + + ```xml Entitlements.plist + com.apple.developer.associated-domains + + applinks:pay.walletconnect.com + + ``` + + **NFC Tag Reading** (for manual CoreNFC scans): + + ```xml Entitlements.plist + com.apple.developer.nfc.readersession.formats + + TAG + + ``` + + Also enable these capabilities in **Xcode → Target → Signing & Capabilities**: + - Associated Domains + - Near Field Communication Tag Reading + + + + Add the NFC usage description (required by Apple for CoreNFC): + + ```xml Info.plist + NFCReaderUsageDescription + This app reads NFC tags to receive payment links from POS terminals. + ``` + + + + This handles the case where the user taps an NFC button inside the app to initiate a read: + + ```swift NFCPaymentReader.swift + import CoreNFC + + final class NFCPaymentReader: NSObject { + + static let shared = NFCPaymentReader() + static var isAvailable: Bool { NFCNDEFReaderSession.readingAvailable } + + private var session: NFCNDEFReaderSession? + private var completion: ((Result) -> Void)? + + func scan(completion: @escaping (Result) -> Void) { + guard NFCNDEFReaderSession.readingAvailable else { + completion(.failure(NFCPaymentError.notAvailable)) + return + } + + self.completion = completion + session = NFCNDEFReaderSession( + delegate: self, + queue: .main, + invalidateAfterFirstRead: true // auto-dismiss after first tag + ) + session?.alertMessage = "Ready to Pay" + session?.begin() + } + } + + extension NFCPaymentReader: NFCNDEFReaderSessionDelegate { + + func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {} + + func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { + for message in messages { + for record in message.records { + if let url = record.wellKnownTypeURIPayload() { + session.alertMessage = "Payment link received!" + session.invalidate() + completion?(.success(url.absoluteString)) + completion = nil + return + } + } + } + session.invalidate(errorMessage: "No payment link found on this NFC tag.") + completion?(.failure(NFCPaymentError.noPaymentLink)) + completion = nil + } + + func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { + if let nfcError = error as? NFCReaderError, + nfcError.code == .readerSessionInvalidationErrorFirstNDEFTagRead || + nfcError.code == .readerSessionInvalidationErrorUserCanceled { + if nfcError.code == .readerSessionInvalidationErrorUserCanceled { + completion?(.failure(NFCPaymentError.cancelled)) + completion = nil + } + return + } + completion?(.failure(error)) + completion = nil + } + } + + enum NFCPaymentError: LocalizedError { + case notAvailable + case noPaymentLink + case cancelled + + var errorDescription: String? { + switch self { + case .notAvailable: return "NFC is not available on this device." + case .noPaymentLink: return "No payment link found on the NFC tag." + case .cancelled: return "NFC scan cancelled." + } + } + } + ``` + + Key points: + - `invalidateAfterFirstRead: true` — iOS dismisses the NFC sheet after one tag is read. + - `wellKnownTypeURIPayload()` — extracts the URI from a standard NDEF URI record (what the POS emits). + - `.readerSessionInvalidationErrorFirstNDEFTagRead` — normal completion, not an error. + + + + This is the zero-interaction path. The user holds their phone near the POS terminal — even from the lock screen or home screen — and iOS reads the NDEF tag automatically, resolves the URL as a Universal Link, and opens your app. + + The URL arrives via `NSUserActivity` in your `SceneDelegate`: + + ```swift SceneDelegate.swift + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + guard let url = userActivity.webpageURL else { return } + + let urlString = url.absoluteString + if WalletKit.isPaymentLink(urlString) { + showPaymentFlow(paymentLink: urlString) + return + } + + // ...handle other Universal Links + } + ``` + + For **cold start** (app was not running), Universal Links arrive in `connectionOptions.userActivities`: + + ```swift SceneDelegate.swift + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { + // ...window setup... + + // Check for payment Universal Link on cold start + if let url = connectionOptions.userActivities + .first(where: { $0.activityType == NSUserActivityTypeBrowsingWeb })? + .webpageURL, + WalletKit.isPaymentLink(url.absoluteString) { + // Delay slightly to let the UI finish loading + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.showPaymentFlow(paymentLink: url.absoluteString) + } + } + } + ``` + + + + Show an NFC scan button only on devices that support it: + + ```swift NFCScanView.swift + // SwiftUI + if NFCPaymentReader.isAvailable { + Button(action: { startNfcScan() }) { + Image(systemName: "wave.3.right") + } + } + + func startNfcScan() { + NFCPaymentReader.shared.scan { result in + switch result { + case .success(let urlString): + if WalletKit.isPaymentLink(urlString) { + showPaymentFlow(paymentLink: urlString) + } + case .failure(let error): + if case NFCPaymentError.cancelled = error { return } + showError(error) + } + } + } + ``` + + + + ```swift PaymentFlow.swift + // 1. Fetch payment options + let accounts = ["eip155:1:0xABC...", "eip155:137:0xABC..."] + let options = try await WalletKit.instance.Pay.getPaymentOptions( + paymentLink: paymentUrl, + accounts: accounts + ) + + // 2. Show options UI — user selects one + + // 3. Get required signing actions + let actions = try await WalletKit.instance.Pay.getRequiredPaymentActions( + paymentId: options.paymentId, + optionId: selectedOption.id + ) + + // 4. Sign each action (typically eth_signTypedData_v4) + let signatures = try actions.map { action in + try yourSigner.sign(method: action.method, params: action.params) + } + + // 5. Confirm payment + let result = try await WalletKit.instance.Pay.confirmPayment( + paymentId: options.paymentId, + optionId: selectedOption.id, + signatures: signatures + ) + ``` + + From ab1e90a70df732a2cb2e457e9303d68ff5f5ca23 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 10:35:09 +0000 Subject: [PATCH 2/2] docs: update description and add BETA tag to Tap to Pay page Co-Authored-By: mirna@reown.com --- payments/wallets/tap-to-pay.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/payments/wallets/tap-to-pay.mdx b/payments/wallets/tap-to-pay.mdx index da0391a..c42ad0a 100644 --- a/payments/wallets/tap-to-pay.mdx +++ b/payments/wallets/tap-to-pay.mdx @@ -1,7 +1,8 @@ --- title: "Tap to Pay - Wallet Integration Guide" -description: "Integrate NFC Tap to Pay into your wallet app on Android and iOS to enable contactless payments at POS terminals using WalletConnect Pay." +description: "Integrate Tap to Pay into your wallet app to enable contactless payments at POS terminals using WalletConnect Pay." sidebarTitle: "Tap to Pay" +tag: "BETA" ---