From a2d29e8e80d37000aed61d36f11982b286481b45 Mon Sep 17 00:00:00 2001 From: "walletconnect-agent[bot]" <287978535+walletconnect-agent[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:28:46 +0000 Subject: [PATCH 1/6] docs: add WebView integration guide for buyer-experience Covers URL construction, query params (mode, callbackScheme, theme, themeVariables), JS bridge messages (WC_PAY_COMPLETE/CANCELLED/ERROR), and platform examples for Android/Kotlin, iOS/Swift, React Native, and Flutter. Wires page into both docs.json nav versions and updates the wallets overview. --- .../wallets/overview.mdxpayments/wallets/webview.mdx | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs.jsonpayments/wallets/overview.mdxpayments/wallets/webview.mdx diff --git a/docs.jsonpayments/wallets/overview.mdxpayments/wallets/webview.mdx b/docs.jsonpayments/wallets/overview.mdxpayments/wallets/webview.mdx new file mode 100644 index 0000000..e69de29 From 1710cd1cc2a2751930e815b71029260ce9b0bc64 Mon Sep 17 00:00:00 2001 From: "walletconnect-agent[bot]" <287978535+walletconnect-agent[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:45:43 +0000 Subject: [PATCH 2/6] docs: correct WebView params and messages from reown-com/react-native-examples#570 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix param names (returnUrl, preferUniversalLinks — not mode/callbackScheme), message types (PAY_SUCCESS/PAY_FAILURE — not WC_PAY_*), add wallet deeplink interception pattern, update all four platform examples. Reference example links to the RN PR. From e2a9ada01c2922803d8e67d0be815aa20bba80de Mon Sep 17 00:00:00 2001 From: "walletconnect-agent[bot]" <287978535+walletconnect-agent[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:28:01 +0000 Subject: [PATCH 3/6] docs: add WebView integration page, nav wiring, and overview card From 1ed470e46215ff26959b6ec40f665143e39ea0c2 Mon Sep 17 00:00:00 2001 From: "walletconnect-agent[bot]" <287978535+walletconnect-agent[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:30:00 +0000 Subject: [PATCH 4/6] docs: add payments/wallets/webview.mdx --- payments/wallets/webview.mdx | 503 +++++++++++++++++++++++++++++++++++ 1 file changed, 503 insertions(+) create mode 100644 payments/wallets/webview.mdx diff --git a/payments/wallets/webview.mdx b/payments/wallets/webview.mdx new file mode 100644 index 0000000..1ba5149 --- /dev/null +++ b/payments/wallets/webview.mdx @@ -0,0 +1,503 @@ +--- +title: "Buyer Experience — WebView Integration" +description: "Embed the WalletConnect Pay checkout UI inside a native WebView so users complete payments without leaving your app." +sidebarTitle: "WebView Integration" +--- + +Instead of redirecting users to an external browser, load the WalletConnect Pay checkout portal inside a native WebView. Supply two query parameters on the `gatewayUrl`, handle JavaScript bridge messages for the outcome, and intercept wallet deeplinks so the OS can route the user to their wallet app and back. + +The hosted UI handles wallet selection, payment option display, compliance data collection, and signing orchestration. Your app only needs to render a WebView, respond to the result, and verify it server-side. + +## How It Works + +```mermaid +sequenceDiagram + participant App as Your App + participant Backend as Your Backend + participant API as WalletConnect Pay API + participant WV as WebView (checkout UI) + participant Wallet as Wallet App + + App->>Backend: User initiates payment + Backend->>API: POST /v1/merchant/payment + API-->>Backend: { paymentId, gatewayUrl } + Backend-->>App: gatewayUrl + + App->>App: Append returnUrl + preferUniversalLinks to gatewayUrl + App->>WV: Load URL in WebView + + Note over WV: User selects payment option,
completes any data collection + + WV->>App: Navigation request — wallet deeplink (?uri=wc:…) + App->>Wallet: Linking.openURL(walletDeeplink) + Wallet-->>App: Returns via returnUrl deep link + + alt Payment succeeds + WV-->>App: postMessage { type: "PAY_SUCCESS", message? } + else Payment fails + WV-->>App: postMessage { type: "PAY_FAILURE", error? } + end + + App->>Backend: Verify payment status + Backend->>API: GET /v1/payments/{paymentId}/status + API-->>Backend: { status: "succeeded" } +``` + +## Prerequisites + +- A `gatewayUrl` from the [Merchant API](/payments/ecommerce/integration) (`POST /v1/merchant/payment`) +- Your app registered to handle its own deep link scheme (so wallets can return after signing) +- JavaScript and DOM storage enabled in the WebView + +## URL Construction + +Start with the `gatewayUrl` the API returns and append the two WebView parameters. **Never construct the base URL manually.** + +```typescript +function buildPayUrl(gatewayUrl: string, appDeepLink: string): string { + const url = new URL(gatewayUrl); + // Required: the wallet returns here after signing + url.searchParams.set('returnUrl', appDeepLink); + // Recommended: open wallets via universal links instead of custom schemes + url.searchParams.set('preferUniversalLinks', '1'); + return url.toString(); +} + +// Example +const payUrl = buildPayUrl( + 'https://pay.walletconnect.com/buy?pid=pay_01ABC', + 'myapp://' // your app's registered deep link +); +``` + +| Parameter | Required | Description | +|---|---|---| +| `returnUrl` | **Yes** | Your app's native deep link (e.g. `myapp://`). The checkout passes this to wallets as the return destination so the OS routes the user back after signing. | +| `preferUniversalLinks` | Recommended | Set to `1`. Tells the checkout to open wallets via universal links (`https://`) rather than custom URL schemes when both are available — more reliable on iOS. | + +## Bridge Messages + +The checkout calls `window.ReactNativeWebView.postMessage(json)` with a JSON string payload. + +| `type` field | `success` field | Meaning | +|---|---|---| +| `PAY_SUCCESS` | `true` | Payment succeeded. Optionally contains `message` — a human-readable confirmation summary. | +| `PAY_FAILURE` | `false` | Payment failed. Optionally contains `error` — a human-readable reason. | + + +The checkout may send either the `type` field or the `success` boolean (or both). Treat `type === "PAY_SUCCESS"` **or** `success === true` as a success signal; `type === "PAY_FAILURE"` **or** `success === false` as failure. + + + +Always verify the final payment status server-side via `GET /v1/payments/{paymentId}/status` after receiving `PAY_SUCCESS`. Never rely solely on the bridge message to confirm a payment. + + +## Wallet Deeplink Interception + +The checkout opens the user's wallet by navigating to a URL that carries a `?uri=wc:…` query parameter. WebViews don't handle these natively, so **your app must intercept these navigations and forward them to the OS**. + +This applies to both same-frame navigations and `window.open()` calls. Only forward URLs that carry a valid `wc:` URI — block any other non-`https:` navigation to prevent the page from driving `Linking.openURL` to arbitrary native schemes (`tel:`, `sms:`, `intent:`, etc.). + +```typescript +function isWalletDeeplink(url: string): boolean { + try { + const wcUri = new URL(url).searchParams.get('uri'); + return !!wcUri && wcUri.startsWith('wc:'); + } catch { + return false; + } +} +``` + +## Platform Examples + + + + +Install the WebView package: + +```bash +npm install react-native-webview +``` + +```tsx +import React, {useCallback} from 'react'; +import {Linking, StyleSheet} from 'react-native'; +import {WebView, WebViewMessageEvent} from 'react-native-webview'; +import type { + ShouldStartLoadRequest, + WebViewOpenWindowEvent, +} from 'react-native-webview/lib/WebViewTypes'; + +type PayMessage = { + type?: 'PAY_SUCCESS' | 'PAY_FAILURE'; + success?: boolean; + error?: string; + message?: string; +}; + +function isWalletDeeplink(url: string): boolean { + try { + const wcUri = new URL(url).searchParams.get('uri'); + return !!wcUri && wcUri.startsWith('wc:'); + } catch { + return false; + } +} + +function buildPayUrl(gatewayUrl: string, appDeepLink: string): string { + const url = new URL(gatewayUrl); + url.searchParams.set('returnUrl', appDeepLink); + url.searchParams.set('preferUniversalLinks', '1'); + return url.toString(); +} + +interface PayWebViewProps { + gatewayUrl: string; + appDeepLink: string; + onSuccess: (message?: string) => void; + onFailure: (error?: string) => void; +} + +export function PayWebView({gatewayUrl, appDeepLink, onSuccess, onFailure}: PayWebViewProps) { + const url = buildPayUrl(gatewayUrl, appDeepLink); + + const onShouldStartLoadWithRequest = useCallback( + (request: ShouldStartLoadRequest): boolean => { + if (isWalletDeeplink(request.url)) { + Linking.openURL(request.url).catch(console.warn); + return false; + } + return true; + }, + [], + ); + + const onOpenWindow = useCallback((event: WebViewOpenWindowEvent) => { + const {targetUrl} = event.nativeEvent; + if (isWalletDeeplink(targetUrl)) { + Linking.openURL(targetUrl).catch(console.warn); + } + }, []); + + const onMessage = useCallback( + (event: WebViewMessageEvent) => { + let msg: PayMessage; + try { + msg = JSON.parse(event.nativeEvent.data); + } catch { + return; + } + if (msg.type === 'PAY_SUCCESS' || msg.success === true) { + onSuccess(msg.message); + } else if (msg.type === 'PAY_FAILURE' || msg.success === false) { + onFailure(msg.error); + } + }, + [onSuccess, onFailure], + ); + + return ( + + ); +} + +const styles = StyleSheet.create({webview: {flex: 1}}); +``` + + +`setSupportMultipleWindows` must be `true` for `onOpenWindow` to receive `window.open()` calls. Without it, wallet deeplinks opened via `window.open()` are silently dropped. + + + + + +```kotlin +import android.content.Intent +import android.net.Uri +import android.webkit.JavascriptInterface +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.runtime.Composable +import androidx.compose.ui.viewinterop.AndroidView +import org.json.JSONObject + +fun buildPayUrl(gatewayUrl: String, appDeepLink: String): String = + Uri.parse(gatewayUrl).buildUpon() + .appendQueryParameter("returnUrl", appDeepLink) + .appendQueryParameter("preferUniversalLinks", "1") + .build().toString() + +fun isWalletDeeplink(url: String): Boolean = try { + Uri.parse(url).getQueryParameter("uri")?.startsWith("wc:") == true +} catch (_: Exception) { false } + +@Composable +fun PayWebView( + gatewayUrl: String, + appDeepLink: String, + onSuccess: (message: String?) -> Unit, + onFailure: (error: String?) -> Unit +) { + AndroidView(factory = { context -> + WebView(context).apply { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.allowFileAccess = false + addJavascriptInterface( + object { + @JavascriptInterface + fun postMessage(json: String) { + try { + val msg = JSONObject(json) + val type = msg.optString("type") + val success = msg.optBoolean("success", false) + when { + type == "PAY_SUCCESS" || (success && type.isEmpty()) -> + onSuccess(msg.optString("message").ifEmpty { null }) + type == "PAY_FAILURE" || (!success && type.isNotEmpty()) -> + onFailure(msg.optString("error").ifEmpty { null }) + } + } catch (_: Exception) {} + } + }, + "ReactNativeWebView" + ) + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + val reqUrl = request?.url?.toString() ?: return false + if (isWalletDeeplink(reqUrl)) { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(reqUrl))) + return true + } + return false + } + } + loadUrl(buildPayUrl(gatewayUrl, appDeepLink)) + } + }) +} +``` + + + + +```swift +import SwiftUI +import WebKit + +struct PayWebView: UIViewRepresentable { + let gatewayUrl: String + let appDeepLink: String + var onSuccess: (String?) -> Void + var onFailure: (String?) -> Void + + func makeCoordinator() -> Coordinator { + Coordinator(onSuccess: onSuccess, onFailure: onFailure) + } + + func makeUIView(context: Context) -> WKWebView { + let cc = WKUserContentController() + cc.add(context.coordinator, name: "ReactNativeWebView") + let config = WKWebViewConfiguration() + config.userContentController = cc + let wv = WKWebView(frame: .zero, configuration: config) + wv.navigationDelegate = context.coordinator + if let url = buildPayURL() { wv.load(URLRequest(url: url)) } + return wv + } + + func updateUIView(_ uiView: WKWebView, context: Context) {} + + private func buildPayURL() -> URL? { + guard var c = URLComponents(string: gatewayUrl) else { return nil } + c.queryItems = (c.queryItems ?? []) + [ + URLQueryItem(name: "returnUrl", value: appDeepLink), + URLQueryItem(name: "preferUniversalLinks", value: "1"), + ] + return c.url + } + + class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate { + var onSuccess: (String?) -> Void + var onFailure: (String?) -> Void + + init(onSuccess: @escaping (String?) -> Void, onFailure: @escaping (String?) -> Void) { + self.onSuccess = onSuccess; self.onFailure = onFailure + } + + func userContentController(_ uc: WKUserContentController, didReceive message: WKScriptMessage) { + guard message.name == "ReactNativeWebView", + let body = message.body as? String, + let data = body.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] + else { return } + let type = json["type"] as? String ?? "" + let success = json["success"] as? Bool + if type == "PAY_SUCCESS" || success == true { onSuccess(json["message"] as? String) } + else if type == "PAY_FAILURE" || success == false { onFailure(json["error"] as? String) } + } + + func webView(_ wv: WKWebView, decidePolicyFor action: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let url = action.request.url, isWalletDeeplink(url) { + UIApplication.shared.open(url); decisionHandler(.cancel); return + } + decisionHandler(.allow) + } + + private func isWalletDeeplink(_ url: URL) -> Bool { + URLComponents(url: url, resolvingAgainstBaseURL: false)? + .queryItems?.first(where: { $0.name == "uri" })?.value?.hasPrefix("wc:") ?? false + } + } +} +``` + + + + +```dart +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +bool isWalletDeeplink(String url) { + try { + return Uri.parse(url).queryParameters['uri']?.startsWith('wc:') ?? false; + } catch (_) { return false; } +} + +String buildPayUrl(String gatewayUrl, String appDeepLink) { + final uri = Uri.parse(gatewayUrl); + return uri.replace(queryParameters: { + ...uri.queryParameters, + 'returnUrl': appDeepLink, + 'preferUniversalLinks': '1', + }).toString(); +} + +class PayWebView extends StatefulWidget { + final String gatewayUrl; + final String appDeepLink; + final void Function(String? message) onSuccess; + final void Function(String? error) onFailure; + + const PayWebView({super.key, required this.gatewayUrl, required this.appDeepLink, + required this.onSuccess, required this.onFailure}); + + @override + State createState() => _PayWebViewState(); +} + +class _PayWebViewState extends State { + late final WebViewController _controller; + + @override + void initState() { + super.initState(); + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onNavigationRequest: (r) { + if (isWalletDeeplink(r.url)) { + launchUrl(Uri.parse(r.url), mode: LaunchMode.externalApplication); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + )) + ..addJavaScriptChannel('ReactNativeWebView', onMessageReceived: (msg) { + try { + final json = jsonDecode(msg.message) as Map; + final type = json['type'] as String? ?? ''; + final success = json['success'] as bool?; + if (type == 'PAY_SUCCESS' || success == true) widget.onSuccess(json['message'] as String?); + else if (type == 'PAY_FAILURE' || success == false) widget.onFailure(json['error'] as String?); + } catch (_) {} + }) + ..loadRequest(Uri.parse(buildPayUrl(widget.gatewayUrl, widget.appDeepLink))); + } + + @override + Widget build(BuildContext context) => WebViewWidget(controller: _controller); +} +``` + + + + +## Deep Link Registration + +Register your `returnUrl` scheme so the OS routes users back after signing in their wallet. + + + + +```xml + + + + + + + +``` + + + + +```xml + +CFBundleURLTypes + + + CFBundleURLSchemes + myapp + + +``` + + + + +## Verifying Payment Status + +After `PAY_SUCCESS`, confirm server-side before marking the order as paid: + +```typescript +const res = await fetch( + `https://api.pay.walletconnect.com/v1/payments/${paymentId}/status`, + { headers: { 'Api-Key': process.env.WCP_API_KEY } } +); +const { status } = await res.json(); // "succeeded" | "processing" | "failed" | … +``` + +See the [Payments Status API](/api-reference/2026-02-18/get-v1-payments-id-status) for the full response schema. + +## Best Practices + +- **Enable JavaScript and DOM storage** — required; the WebView will not render correctly without them. +- **Always intercept wallet deeplinks** — URLs with `?uri=wc:…` must be forwarded to the OS. Handle both same-frame navigations and `window.open()` calls. +- **Only intercept `wc:` deeplinks** — block other non-`https:` navigations to prevent arbitrary native app launches. +- **Handle both message formats** — check `type === "PAY_SUCCESS"` **and** `success === true` for forward compatibility. +- **Always verify server-side** — treat bridge messages as a trigger to check status, not as proof of payment. +- **Keep the WebView full-screen or modal** — the checkout is optimized for full viewport rendering. + +## Reference Example + + + Full implementation including success animation, wallet deeplink handling, and error recovery. + From dc180a258acfd2405be8d67b9d9494ea2dba4694 Mon Sep 17 00:00:00 2001 From: "walletconnect-agent[bot]" <287978535+walletconnect-agent[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:30:27 +0000 Subject: [PATCH 5/6] docs: wire payments/wallets/webview into docs.json nav (both versions) --- docs.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs.json b/docs.json index 00bc2de..b75001b 100644 --- a/docs.json +++ b/docs.json @@ -85,6 +85,7 @@ ] }, "payments/wallets/api-first", + "payments/wallets/webview", { "group": "Token & Chain Support", "pages": [ @@ -242,6 +243,7 @@ ] }, "payments/wallets/api-first", + "payments/wallets/webview", { "group": "Token & Chain Support", "pages": [ From e896787efe2245d40f736010d75ad0dfb7c1a992 Mon Sep 17 00:00:00 2001 From: "walletconnect-agent[bot]" <287978535+walletconnect-agent[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:30:50 +0000 Subject: [PATCH 6/6] docs: add WebView integration card to wallets overview --- payments/wallets/overview.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/payments/wallets/overview.mdx b/payments/wallets/overview.mdx index 83d57c2..58e4b49 100644 --- a/payments/wallets/overview.mdx +++ b/payments/wallets/overview.mdx @@ -71,6 +71,16 @@ For wallets that prefer direct API integration without using an SDK, you can use +### WebView Integration + +For apps that want to embed the full WalletConnect Pay checkout UI inside a native WebView — without redirecting to an external browser. The hosted checkout handles everything; your app supplies two query parameters and listens for bridge messages. + + + + Embed the checkout portal in a native WebView (iOS, Android, React Native, Flutter). + + + ## Getting Started To get started with WalletConnect Pay integration: