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: