diff --git a/api-reference/index.mdx b/api-reference/index.mdx index 68a7fe8..ac7df33 100644 --- a/api-reference/index.mdx +++ b/api-reference/index.mdx @@ -8,10 +8,8 @@ This section contains the **Gateway API** endpoints needed for an **API-first (n ## WalletConnect Pay (Gateway) -- **Get payment options** -- **Fetch an action** -- **Confirm a payment** - -If you’re looking for the integration overview, start with **[API-first integration (Non-SDK wallets)](/payments/wallets/api-first)**. - +- **Get payment options** — list available options for the user's accounts +- **Fetch an action** — resolve build actions into executable wallet RPC actions +- **Confirm a payment** — submit signatures and complete the payment +If you're looking for the integration overview, start with **[API-first integration (Non-SDK wallets)](/payments/wallets/api-first)**. diff --git a/api/walletconnect-pay-gateway.yaml b/api/walletconnect-pay-gateway.yaml index 1c01885..9910840 100644 --- a/api/walletconnect-pay-gateway.yaml +++ b/api/walletconnect-pay-gateway.yaml @@ -19,19 +19,9 @@ paths: required: true schema: type: string - - name: Sdk-Name + - name: App-Id in: header - required: true - schema: - type: string - - name: Sdk-Version - in: header - required: true - schema: - type: string - - name: Sdk-Platform - in: header - required: true + required: false schema: type: string - name: id @@ -97,19 +87,9 @@ paths: required: true schema: type: string - - name: Sdk-Name - in: header - required: true - schema: - type: string - - name: Sdk-Version + - name: App-Id in: header - required: true - schema: - type: string - - name: Sdk-Platform - in: header - required: true + required: false schema: type: string - name: id @@ -172,7 +152,7 @@ paths: tags: - Gateway summary: Confirm a payment - description: This endpoint confirms a payment and submits it to the blockchain for processing. + description: "This endpoint confirms a payment and submits it to the blockchain for processing." operationId: confirm_payment_handler parameters: - name: Api-Key @@ -180,19 +160,9 @@ paths: required: true schema: type: string - - name: Sdk-Name + - name: App-Id in: header - required: true - schema: - type: string - - name: Sdk-Version - in: header - required: true - schema: - type: string - - name: Sdk-Platform - in: header - required: true + required: false schema: type: string - name: id @@ -223,6 +193,8 @@ paths: value: John Smith - id: dob value: "1990-01-01" + - id: tosConfirmed + value: "true" optionId: opt_123 results: - data: @@ -331,10 +303,21 @@ components: type: string nullable: true description: URL of the icon of the asset (if token) + networkIconUrl: + type: string + nullable: true + description: URL of the icon of the network networkName: type: string nullable: true description: Name of the network of the asset (if token) + Build: + type: object + required: + - data + properties: + data: + type: string BuyerInfo: type: object required: @@ -351,18 +334,16 @@ components: accountProviderName: type: string description: Account provider name - Build: - type: object - required: - - data - properties: - data: - type: string CollectData: type: object - required: - - fields properties: + url: + type: string + description: WebView URL for data collection + schema: + type: object + nullable: true + description: JSON schema describing the required fields fields: type: array items: @@ -476,9 +457,6 @@ components: - missing_api_key - missing_merchant_api_key - missing_merchant_id - - missing_sdk_name - - missing_sdk_version - - missing_sdk_platform - header_not_ascii - not_sandbox_api_key - merchant_not_found @@ -596,6 +574,9 @@ components: - etaS - actions properties: + account: + type: string + description: CAIP-10 account identifier for this option actions: type: array items: @@ -642,5 +623,3 @@ components: name: Api-Key security: - API Key: [] - - diff --git a/payments/wallets/api-first.mdx b/payments/wallets/api-first.mdx index 0fd5b03..411eefe 100644 --- a/payments/wallets/api-first.mdx +++ b/payments/wallets/api-first.mdx @@ -4,28 +4,27 @@ sidebarTitle: "API-first (No SDK)" description: "Integrate WalletConnect Pay in a wallet without an SDK, using the Gateway API." --- -If you’re integrating WalletConnect Pay into a wallet **without using the Wallet Pay SDK**, you can use an API-first approach via the **Gateway API**. +If you're integrating WalletConnect Pay into a wallet **without using the Wallet Pay SDK**, you can use an API-first approach via the **Gateway API**. This flow is centered around **three Gateway calls**: - **Get payment options**: list options the user can complete with their wallet/accounts -- **Fetch an action**: resolve “build” actions into wallet RPC actions when needed +- **Fetch an action**: resolve "build" actions into wallet RPC actions when needed - **Confirm a payment**: submit the selected option and the executed action results ## Prerequisites - **API key**: request access from WalletConnect - You can do this by filling out [**this form**](https://share.hsforms.com/19Dpp4ayYR9uriB3xNAh0JAnxw6s) and getting in touch with our team. -- **Wallet identity headers** (required on each request): - - `Sdk-Name` - - `Sdk-Version` - - `Sdk-Platform` +- **Required headers** on each request: + - `Api-Key` — your API key + ## Payment flow The payment flow mirrors the SDK flow, but you call the Gateway API directly: -**Get Options → (Fetch Actions) → Execute Wallet RPC → (Collect Data) → Confirm Payment** +**Get Options → (Collect Data) → (Fetch Actions) → Execute Wallet RPC → Confirm Payment** ```mermaid sequenceDiagram @@ -39,6 +38,11 @@ sequenceDiagram Gateway-->>Wallet: options[] (+ optional info/collectData) Wallet->>User: Display payment options + alt Data collection required + Wallet->>User: Request additional info (WebView) + User->>Wallet: Complete form + end + User->>Wallet: Select payment option alt Option includes a build action @@ -51,11 +55,6 @@ sequenceDiagram Wallet->>Chain: Execute wallet RPC actions (in order) Chain-->>Wallet: Results (e.g., signature(s) / tx hash(es)) - alt Data collection required - Wallet->>User: Request additional info - User->>Wallet: Provide data - end - Wallet->>Gateway: POST /v1/gateway/payment/{id}/confirm (optionId, results, collectedData?) Gateway-->>Wallet: status + isFinal (+ pollInMs) Wallet->>User: Show result / status @@ -97,15 +96,116 @@ If an option contains an action of type **`build`**, call **Fetch an action** wi After your wallet executes the required `walletRpc` actions (sign/submit), call **Confirm a payment** with: - the selected `optionId` -- `results[]`: the output from each executed action (in the same spirit/order you performed them) +- `results[]`: the output from each executed action (in the same order you performed them) - optional `collectedData` if the options response requested additional user info + +If you used the **WebView-based data collection** flow (i.e., displayed `collectData.url` in a WebView), there is no need to send `collectedData` in the confirm request — the WebView submits user data directly to the backend. + + If `isFinal` is `false`, the response may include `pollInMs`. Use it to decide when to check again (see the API Reference for status/polling behavior). -## API Reference + +The WalletConnect Pay SDKs support **WebView-based data collection**. When `collectData.url` is present in the payment options response, wallets can display this URL in a WebView instead of building native forms. The WebView handles form rendering, validation, and T&C acceptance, and submits data directly to the backend. See the [platform-specific SDK documentation](/payments/wallets/overview) for implementation details. + -For request/response schemas and examples for each Gateway endpoint, see the **[API Reference](/api-reference)**. +## Integration guidelines + +These guidelines reflect the patterns used internally by the WalletConnect Pay SDK. Following them ensures a smooth, reliable UX. + +### Payment link detection + +Payment links can arrive in several formats. Your wallet should detect and extract the `paymentId` from: + +| Format | Example | +|--------|---------| +| WC Pay URL (path) | `https://pay.walletconnect.com/pay_123` | +| WC Pay URL (query) | `https://pay.walletconnect.com/?pid=pay_123` | +| `wc:` URI with `pay=` param | `wc:abc@2?pay=https%3A%2F%2Fpay.walletconnect.com%2F%3Fpid%3Dpay_123` | +| Bare payment ID | `pay_123` | + + +Only trust `pay.walletconnect.com` and `*.pay.walletconnect.com` as valid WC Pay hosts. Always validate the domain before extracting a payment ID. + + + +Check for payment links **before** handling generic URLs or WalletConnect pairing URIs. Payment links are HTTPS URLs that would otherwise open in a browser. + + +### Providing accounts + +Pass all of the user's accounts in **CAIP-10 format** (`eip155:{chainId}:{address}`) when calling **Get payment options**. Include accounts for every supported chain to maximize the number of payment options returned. + +### Resolving `build` actions + +When an option's `actions[]` contains a `build` action, it cannot be executed directly. Call **Fetch an action** (`POST /v1/gateway/payment/{id}/fetch`) with the `optionId` and the build action's `data` string. The response will contain one or more `walletRpc` actions that your wallet can execute. + + +A single `build` action may resolve into multiple `walletRpc` actions. Always iterate over the full response. + + +### Executing wallet RPC actions +Each `walletRpc` action contains: +- `chain_id` — the chain to execute on (CAIP-2 format, e.g., `eip155:8453`) +- `method` — the RPC method (e.g., `eth_signTypedData_v4`, `personal_sign`) +- `params` — JSON-encoded parameters +Execute each action in order using your wallet's signing implementation, and collect the result (e.g., signature hex string) for each. + +### Submitting results + +When calling **Confirm a payment**, wrap each signature as a `walletRpc` result: + +```json +{ + "optionId": "opt_123", + "results": [ + { "type": "walletRpc", "data": ["0x"] }, + { "type": "walletRpc", "data": ["0x"] } + ] +} +``` + + +The `results[]` array **must** match the `actions[]` array in both length and order. Misalignment causes payment failures. + + +### Polling for final status + +After **Confirm a payment** returns, check the `isFinal` field: + +- **`isFinal: true`** — the payment has reached a terminal state (`succeeded`, `failed`, or `expired`). No further action needed. +- **`isFinal: false`** — the payment is still processing. Use the `pollInMs` value from the response as the delay before your next confirm call. + +Use the `maxPollMs` query parameter on the confirm request to enable **server-side long-polling** — the server will hold the connection and return as soon as the status changes or the timeout expires, reducing the number of round-trips. + +``` +POST /v1/gateway/payment/{id}/confirm?maxPollMs=30000 +``` + +### Retry strategy + +Implement retries with **exponential backoff and jitter** for resilience: + +- **Retry only on** server errors (5xx) and network failures (connection refused, timeout) +- **Do not retry** client errors (4xx) — these indicate invalid input and won't succeed on retry +- **Recommended**: 3 retries with 100ms initial backoff, doubling each attempt, plus random jitter + +### Data collection + +If the **Get payment options** response includes a `collectData` object with a `url` field, display it in a WebView before confirming. The WebView handles form rendering, validation, and T&C acceptance. When the WebView signals completion (`IC_COMPLETE` via JS bridge), proceed to confirm — no need to include `collectedData` in the request. + +If you choose not to use the WebView and instead build your own form, use the `collectData.schema` JSON schema to determine the required fields, collect the values, and pass them as `collectedData` in the confirm request. + +### Expiration handling + +Payments have an expiration timestamp (`expiresAt` in the payment info). Display a countdown or warning to the user when time is running low, and prevent submission after expiry. + +If a payment or route expires mid-flow, the API returns a `410` (payment expired) or `409` (route expired) error. Handle these gracefully by informing the user and offering to start over if a new payment link is available. + +## API Reference + +For request/response schemas and examples for each Gateway endpoint, see the **[API Reference](/api-reference)**. diff --git a/payments/wallets/overview.mdx b/payments/wallets/overview.mdx index 50b7504..932db35 100644 --- a/payments/wallets/overview.mdx +++ b/payments/wallets/overview.mdx @@ -75,7 +75,7 @@ Regardless of which integration approach you choose, the payment flow follows th 2. **Get Payment Options**: Fetch available payment options based on user's accounts 3. **Get Required Actions**: Retrieve the signing actions for the selected payment option 4. **Sign Actions**: Sign the required permits/transactions using your wallet's signing infrastructure -5. **Collect User Data** (if required): Gather any additional user information for compliance +5. **Collect User Data** (if required): Display a WebView for the user to provide any required compliance information 6. **Confirm Payment**: Submit signatures and complete the payment ## Supported Networks @@ -102,14 +102,15 @@ Support for all EVM chains, Solana, and additional native and non-native assets - Wallets that already have verified user PII (e.g. a neobank, a card issuing wallet) can pass this information directly. The user will see just the payment flow without additional data collection steps. + Wallets that already have verified user PII (e.g. a neobank, a card issuing wallet) can prefill the WebView form by appending a `?prefill=` query parameter to the WebView URL. The `required` list from the `collectDataAction.schema` tells you which fields the form expects (e.g., `fullName`, `dateOfBirth`, `pobAddress`). The user will still see the form but with pre-populated fields, reducing friction. When processing a payment: - 1. Check if `response.collectData` is present - 2. If present and you don't already have the required user information, collect it from the user - 3. If present and you already have the required user information, skip collection and submit the data directly with the signatures + 1. Check if `response.collectData` is present and has a `url` field + 2. If present, display the URL in a WebView — the hosted form handles rendering, validation, and T&C acceptance + 3. The WebView communicates completion via JavaScript bridge messages (`IC_COMPLETE` / `IC_ERROR`) + 4. When the WebView completes, proceed to confirm the payment — no `collectedData` needs to be passed since the WebView submits data directly @@ -117,7 +118,40 @@ Support for all EVM chains, Solana, and additional native and non-native assets - Whether you are collecting the information or not, before submitting user information to WalletConnect, you must ensure the user has accepted: + The WebView-based data collection form handles Terms & Conditions and Privacy Policy acceptance as part of the form flow. The user must accept these before the form can be submitted. + + + + SDKs now support a WebView-based approach for collecting user information. When `collectData.url` is present in the payment options response, wallets display this URL in a WebView instead of building native forms. The hosted form handles: + - Form rendering and field validation + - Terms & Conditions and Privacy Policy acceptance + - Data submission directly to the backend + + The WebView communicates with the wallet via JavaScript bridge messages (`IC_COMPLETE` when done, `IC_ERROR` on failure). See the platform-specific documentation for implementation details. + + We strongly recommend using the WebView-based flow over building custom native UI. Data collection requirements are driven by regulation and can evolve. The hosted WebView form is maintained and updated centrally so that wallets can automatically pick up changes. + + + + Yes, with a caveat. + + Wallets that choose to implement custom UI assume responsibility for keeping their forms in sync with these changes. When changes are rolled out, wallets must implement them immediately. If the provided data is not what is requested, users in the relevant jurisdiction(s) will be unable to pay until the custom integration is updated. The WebView approach eliminates this overhead and ensures a consistent, up-to-date experience with minimal integration effort. + + Wallets also must ensure the user has accepted: + - [WalletConnect Terms and Conditions](https://walletconnect.com/terms) + - [WalletConnect Privacy Policy](https://walletconnect.com/privacy) + + The required user information can be sent to WalletConnect using schema in the `collect_data` object. + + We strongly recommend using the WebView-based flow over building custom native UI. Data collection requirements are driven by regulation and can evolve. The hosted WebView form is maintained and updated centrally so that wallets can automatically pick up changes. + + + + Yes. + + Wallets can pre-fill user information and it will be shown in the WebView form. + + If wallets choose to skip the WebView form, they must ensure the user has accepted: - [WalletConnect Terms and Conditions](https://walletconnect.com/terms) - [WalletConnect Privacy Policy](https://walletconnect.com/privacy) diff --git a/payments/wallets/standalone/flutter.mdx b/payments/wallets/standalone/flutter.mdx index 0eafb57..354d29b 100644 --- a/payments/wallets/standalone/flutter.mdx +++ b/payments/wallets/standalone/flutter.mdx @@ -4,6 +4,10 @@ description: "Integrate WalletConnect Pay into your Flutter wallet to enable sea sidebarTitle: "Flutter" --- +import WebViewOverview from "/snippets/webview-data-collection-overview.mdx"; +import WebViewBestPractices from "/snippets/webview-data-collection-best-practices.mdx"; +import WebViewMessageTypes from "/snippets/webview-message-types.mdx"; + The WalletConnect Pay SDK allows wallet users to pay merchants using their crypto assets. The SDK handles payment option discovery, permit signing coordination, and payment confirmation while leveraging your wallet's existing signing infrastructure. ## Requirements @@ -88,6 +92,7 @@ sequenceDiagram participant Wallet participant PaySDK as Pay SDK participant Backend as WalletConnect Pay + participant WebView User->>Wallet: Scan QR / Open payment link Wallet->>PaySDK: getPaymentOptions(link, accounts) @@ -95,21 +100,23 @@ sequenceDiagram Backend-->>PaySDK: Payment options + merchant info PaySDK-->>Wallet: PaymentOptionsResponse Wallet->>User: Display payment options - + User->>Wallet: Select payment option Wallet->>PaySDK: getRequiredPaymentActions(paymentId, optionId) PaySDK->>Backend: Get signing actions Backend-->>PaySDK: Required wallet RPC actions PaySDK-->>Wallet: List of actions to sign - + Wallet->>User: Request signature(s) User->>Wallet: Approve & sign - + alt Data collection required - Wallet->>User: Request additional info - User->>Wallet: Provide data + Wallet->>WebView: Load collectDataAction.url in WebView + WebView->>User: Display data collection form + User->>WebView: Fill form & accept T&C + WebView-->>Wallet: IC_COMPLETE message end - + Wallet->>PaySDK: confirmPayment(signatures, collectedData) PaySDK->>Backend: Submit payment Backend-->>PaySDK: Payment status @@ -181,19 +188,30 @@ for (final action in actions) { Some payments may require additional user data. Check for `collectData` in the payment options response: -```dart -List? collectedData; + -if (response.collectData != null) { - collectedData = response.collectData!.fields.map((field) { - return CollectDataFieldResult( - id: field.id, - value: getUserInput(field.name, field.fieldType), - ); - }).toList(); +```dart +if (response.collectData?.url != null) { + // Use the "required" list from response.collectData.schema to determine which fields to prefill + final prefillData = { + 'fullName': 'John Doe', + 'dateOfBirth': '1990-01-15', + 'pobAddress': '123 Main St, New York, NY 10001', + }; + final prefillJson = jsonEncode(prefillData); + final prefillBase64 = base64Url.encode(utf8.encode(prefillJson)); + final uri = Uri.parse(response.collectData!.url); + final webViewUrl = uri.replace( + queryParameters: {...uri.queryParameters, 'prefill': prefillBase64}, + ).toString(); + + // Show WebView — see WebView Implementation section below + showDataCollectionWebView(webViewUrl); } ``` + + @@ -234,6 +252,112 @@ if (!confirmResponse.isFinal && confirmResponse.pollInMs != null) { +## WebView Implementation + +When `collectData.url` is present, display the URL in a WebView using the `webview_flutter` package (v4.10.0+). Add it to your `pubspec.yaml`: + +```yaml +dependencies: + webview_flutter: ^4.10.0 + url_launcher: ^6.1.0 +``` + +```dart +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class PayDataCollectionWebView extends StatefulWidget { + final String url; + final VoidCallback onComplete; + final ValueChanged onError; + + const PayDataCollectionWebView({ + super.key, + required this.url, + required this.onComplete, + required this.onError, + }); + + @override + State createState() => + _PayDataCollectionWebViewState(); +} + +class _PayDataCollectionWebViewState extends State { + late final WebViewController _controller; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => setState(() => _isLoading = false), + onNavigationRequest: (request) { + if (!request.url.contains('pay.walletconnect.com')) { + launchUrl(Uri.parse(request.url), + mode: LaunchMode.externalApplication); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + )) + ..addJavaScriptChannel( + 'ReactNativeWebView', + onMessageReceived: (message) { + try { + final data = jsonDecode(message.message) as Map; + switch (data['type']) { + case 'IC_COMPLETE': + widget.onComplete(); + break; + case 'IC_ERROR': + widget.onError(data['error'] ?? 'Unknown error'); + break; + } + } catch (_) { + // Ignore non-JSON messages + } + }, + ) + ..loadRequest(Uri.parse(widget.url)); + + // Inject JS bridge for compatibility + _controller.runJavaScript(''' + window.ReactNativeWebView = { + postMessage: function(data) { + ReactNativeWebView.postMessage(data); + } + }; + '''); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + WebViewWidget(controller: _controller), + if (_isLoading) + const Center(child: CircularProgressIndicator()), + ], + ); + } +} + +String buildPrefillUrl(String baseUrl, Map prefillData) { + if (prefillData.isEmpty) return baseUrl; + final json = jsonEncode(prefillData); + final base64 = base64Url.encode(utf8.encode(json)); + final uri = Uri.parse(baseUrl); + return uri.replace( + queryParameters: {...uri.queryParameters, 'prefill': base64}, + ).toString(); +} +``` + ## Complete Example Here's a complete implementation example: @@ -286,10 +410,10 @@ class PaymentService { signatures.add(signature); } - // Step 5: Collect user data if required - List? collectedData; - if (optionsResponse.collectData != null) { - collectedData = await collectUserData(optionsResponse.collectData!.fields); + // Step 5: Collect data via WebView if required + if (optionsResponse.collectData?.url != null) { + // Show WebView and wait for IC_COMPLETE + await showDataCollectionWebView(optionsResponse.collectData!.url); } // Step 6: Confirm payment @@ -298,7 +422,6 @@ class PaymentService { paymentId: optionsResponse.paymentId, optionId: selectedOption.id, signatures: signatures, - collectedData: collectedData, maxPollMs: 60000, ), ); @@ -312,7 +435,6 @@ class PaymentService { paymentId: optionsResponse.paymentId, optionId: selectedOption.id, signatures: signatures, - collectedData: collectedData, maxPollMs: 60000, ), ); @@ -326,18 +448,6 @@ class PaymentService { // Use walletRpc.chainId, walletRpc.method, walletRpc.params throw UnimplementedError('Implement signing logic'); } - - Future> collectUserData( - List fields, - ) async { - // Implement your UI to collect user data - return fields.map((field) { - return CollectDataFieldResult( - id: field.id, - value: getUserInput(field), - ); - }).toList(); - } } ``` @@ -387,9 +497,19 @@ PaymentOptionsResponse({ PaymentInfo? info, required List options, CollectDataAction? collectData, + PaymentResultInfo? resultInfo, // Transaction result details (present when payment already completed) }) ``` +### PaymentResultInfo + +```dart +class PaymentResultInfo { + final String txId; // Transaction ID + final PayAmount optionAmount; // Token amount details +} +``` + ### PaymentInfo ```dart @@ -462,24 +582,12 @@ class WalletRpcAction { } ``` -### CollectDataField & CollectDataFieldResult +### CollectDataAction ```dart -class CollectDataField { - final String id; - final String name; - final bool required; - final CollectDataFieldType fieldType; -} - -class CollectDataFieldResult { - final String id; - final String value; -} - -enum CollectDataFieldType { - text, - date, +class CollectDataAction { + final String url; // WebView URL for data collection + final String? schema; // JSON schema describing required fields } ``` @@ -551,6 +659,8 @@ try { 8. **User Data**: Only collect data when `collectData` is present in the response and you don't already have the required user data. If you already have the required data, you can submit this without collecting from the user. You must make sure the user accepts WalletConnect Terms and Conditions and Privacy Policy before submitting user information to WalletConnect. +9. **WebView Data Collection**: When `collectData.url` is present, display the URL in a WebView using `webview_flutter` rather than building native forms. The WebView handles form rendering, validation, and T&C acceptance. + ## Examples For a complete example implementation, see the [reown_walletkit example](https://github.com/reown-com/reown_flutter/tree/master/packages/reown_walletkit/example/lib/walletconnect_pay). diff --git a/payments/wallets/standalone/kotlin.mdx b/payments/wallets/standalone/kotlin.mdx index 2e453c8..d94fc7b 100644 --- a/payments/wallets/standalone/kotlin.mdx +++ b/payments/wallets/standalone/kotlin.mdx @@ -4,6 +4,10 @@ description: "Integrate WalletConnect Pay into your Android wallet to enable sea sidebarTitle: "Kotlin" --- +import WebViewOverview from "/snippets/webview-data-collection-overview.mdx"; +import WebViewBestPractices from "/snippets/webview-data-collection-best-practices.mdx"; +import WebViewMessageTypes from "/snippets/webview-message-types.mdx"; + The WalletConnect Pay SDK allows wallet users to pay merchants using their crypto assets. The SDK handles payment option discovery, permit signing coordination, and payment confirmation while leveraging your wallet's existing signing infrastructure. ## Requirements @@ -104,6 +108,7 @@ sequenceDiagram participant Wallet participant PaySDK as Pay SDK participant Backend as WalletConnect Pay + participant WebView User->>Wallet: Scan QR / Open payment link Wallet->>PaySDK: getPaymentOptions(link, accounts) @@ -111,21 +116,23 @@ sequenceDiagram Backend-->>PaySDK: Payment options + merchant info PaySDK-->>Wallet: PaymentOptionsResponse Wallet->>User: Display payment options - + User->>Wallet: Select payment option Wallet->>PaySDK: getRequiredPaymentActions(paymentId, optionId) PaySDK->>Backend: Get signing actions Backend-->>PaySDK: Required wallet RPC actions PaySDK-->>Wallet: List of actions to sign - + Wallet->>User: Request signature(s) User->>Wallet: Approve & sign - + alt Data collection required - Wallet->>User: Request additional info - User->>Wallet: Provide data + Wallet->>WebView: Load collectDataAction.url in WebView + WebView->>User: Display data collection form + User->>WebView: Fill form & accept T&C + WebView-->>Wallet: IC_COMPLETE message end - + Wallet->>PaySDK: confirmPayment(signatures, collectedData) PaySDK->>Backend: Submit payment Backend-->>PaySDK: Payment status @@ -251,21 +258,35 @@ Signatures must be in the same order as the actions array. Some payments require collecting additional user information. Check for `collectDataAction` in the payment options response: + + ```kotlin response.collectDataAction?.let { collectAction -> - val collectedData = collectAction.fields.map { field -> - val value = when (field.fieldType) { - Pay.CollectDataFieldType.TEXT -> getUserTextInput(field.name) - Pay.CollectDataFieldType.DATE -> getUserDateInput(field.name) // Format: YYYY-MM-DD - } - Pay.CollectDataFieldResult( - id = field.id, - value = value + val url = collectAction.url + if (url != null) { + // Build prefill URL with known user data + // Use the "required" list from collectAction.schema to determine which fields to prefill + val prefillJson = JSONObject().apply { + put("fullName", "John Doe") + put("dateOfBirth", "1990-01-15") + put("pobAddress", "123 Main St, New York, NY 10001") + }.toString() + val prefillBase64 = Base64.encodeToString( + prefillJson.toByteArray(), + Base64.NO_WRAP or Base64.URL_SAFE ) + val webViewUrl = Uri.parse(url).buildUpon() + .appendQueryParameter("prefill", prefillBase64) + .build().toString() + + // Show WebView — see WebView Implementation section below + showWebView(webViewUrl) } } ``` + + @@ -308,6 +329,83 @@ confirmResult.onSuccess { response -> +## WebView Implementation + +When `collectDataAction.url` is present, display the URL in a WebView. The WebView handles form rendering, validation, and T&C acceptance. + +```kotlin +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient +import android.webkit.WebResourceRequest +import android.content.Intent +import android.net.Uri +import android.util.Base64 +import androidx.compose.runtime.Composable +import androidx.compose.ui.viewinterop.AndroidView +import org.json.JSONObject + +@Composable +fun PayDataCollectionWebView( + url: String, + onComplete: () -> Unit, + onError: (String) -> Unit +) { + AndroidView(factory = { context -> + WebView(context).apply { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.allowFileAccess = false + + addJavascriptInterface( + object { + @JavascriptInterface + fun onDataCollectionComplete(json: String) { + val message = JSONObject(json) + when (message.optString("type")) { + "IC_COMPLETE" -> onComplete() + "IC_ERROR" -> onError( + message.optString("error", "Unknown error") + ) + } + } + }, + "AndroidWallet" + ) + + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val requestUrl = request?.url?.toString() ?: return false + // Open external links (T&C, Privacy Policy) in system browser + if (!requestUrl.contains("pay.walletconnect.com")) { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(requestUrl))) + return true + } + return false + } + } + + loadUrl(url) + } + }) +} + +fun buildPrefillUrl(baseUrl: String, prefillData: Map): String { + if (prefillData.isEmpty()) return baseUrl + val json = JSONObject(prefillData).toString() + val base64 = Base64.encodeToString( + json.toByteArray(), + Base64.NO_WRAP or Base64.URL_SAFE + ) + return Uri.parse(baseUrl).buildUpon() + .appendQueryParameter("prefill", base64) + .build().toString() +} +``` + ## Complete Example Here's a complete implementation example using a ViewModel: @@ -354,17 +452,18 @@ class PaymentViewModel : ViewModel() { // Step 3: Sign actions val signatures = signActions(actions) - // Step 4: Collect data if required - val collectedData = response.collectDataAction?.let { - collectUserData(it.fields) + // Step 4: Collect data if required (via WebView) + response.collectDataAction?.url?.let { webViewUrl -> + // Show WebView and wait for IC_COMPLETE + showDataCollectionWebView(webViewUrl) + return@launch // Resume after WebView completes } // Step 5: Confirm payment val confirmResult = WalletConnectPay.confirmPayment( paymentId = paymentId, optionId = selectedOption.id, - signatures = signatures, - collectedData = collectedData + signatures = signatures ) confirmResult.onSuccess { confirmation -> @@ -423,7 +522,7 @@ Main entry point for the Pay SDK (singleton object). | `initialize(config: Pay.SdkConfig)` | Initialize the SDK | | `getPaymentOptions(paymentLink, accounts)` | Get available payment options | | `getRequiredPaymentActions(paymentId, optionId)` | Get actions requiring signatures | -| `confirmPayment(paymentId, optionId, signatures, collectedData?)` | Confirm and finalize payment | +| `confirmPayment(paymentId, optionId, signatures)` | Confirm and finalize payment | ## Data Models @@ -434,7 +533,13 @@ data class PaymentOptionsResponse( val info: PaymentInfo?, // Payment metadata val options: List, // Available payment options val paymentId: String, // Unique payment identifier - val collectDataAction: CollectDataAction? // Data collection requirements + val collectDataAction: CollectDataAction?, // Data collection requirements + val resultInfo: PaymentResultInfo? // Transaction result details (present when payment already completed) +) + +data class PaymentResultInfo( + val txId: String, // Transaction ID + val optionAmount: Amount // Token amount details ) ``` @@ -502,6 +607,15 @@ sealed class RequiredAction { } ``` +### Pay.CollectDataAction + +```kotlin +data class CollectDataAction( + val url: String, // WebView URL for data collection + val schema: String? // JSON schema describing required fields +) +``` + ### Pay.ConfirmPaymentResponse ```kotlin @@ -604,3 +718,5 @@ result.onFailure { error -> 5. **Error Handling**: Always handle errors gracefully and show appropriate user feedback 6. **Thread Safety**: Events are delivered on IO dispatcher; update UI on main thread + +7. **WebView Data Collection**: When `collectDataAction.url` is present, display the URL in a WebView rather than building native forms. The WebView handles form rendering, validation, and T&C acceptance. diff --git a/payments/wallets/standalone/react-native.mdx b/payments/wallets/standalone/react-native.mdx index cc070e1..7105ebb 100644 --- a/payments/wallets/standalone/react-native.mdx +++ b/payments/wallets/standalone/react-native.mdx @@ -4,6 +4,10 @@ description: "Integrate WalletConnect Pay into your React Native wallet to enabl sidebarTitle: "React Native" --- +import WebViewOverview from "/snippets/webview-data-collection-overview.mdx"; +import WebViewBestPractices from "/snippets/webview-data-collection-best-practices.mdx"; +import WebViewMessageTypes from "/snippets/webview-message-types.mdx"; + The WalletConnect Pay SDK allows wallet users to pay merchants using their crypto assets. The SDK handles payment option discovery, permit signing coordination, and payment confirmation while leveraging your wallet's existing signing infrastructure. ## Requirements @@ -105,6 +109,7 @@ sequenceDiagram participant Wallet participant PaySDK as Pay SDK participant Backend as WalletConnect Pay + participant WebView User->>Wallet: Scan QR / Open payment link Wallet->>PaySDK: getPaymentOptions(link, accounts) @@ -112,21 +117,23 @@ sequenceDiagram Backend-->>PaySDK: Payment options + merchant info PaySDK-->>Wallet: PaymentOptionsResponse Wallet->>User: Display payment options - + User->>Wallet: Select payment option Wallet->>PaySDK: getRequiredPaymentActions(paymentId, optionId) PaySDK->>Backend: Get signing actions Backend-->>PaySDK: Required wallet RPC actions PaySDK-->>Wallet: List of actions to sign - + Wallet->>User: Request signature(s) User->>Wallet: Approve & sign - + alt Data collection required - Wallet->>User: Request additional info - User->>Wallet: Provide data + Wallet->>WebView: Load collectDataAction.url in WebView + WebView->>User: Display data collection form + User->>WebView: Fill form & accept T&C + WebView-->>Wallet: IC_COMPLETE message end - + Wallet->>PaySDK: confirmPayment(signatures, collectedData) PaySDK->>Backend: Submit payment Backend-->>PaySDK: Payment status @@ -213,18 +220,27 @@ Signatures must be in the same order as the actions array. Some payments may require additional user data. Check for `collectData` in the payment options response: -```typescript -let collectedData: CollectDataFieldResult[] | undefined; + -if (options.collectData) { - // Show UI to collect required fields - collectedData = options.collectData.fields.map((field) => ({ - id: field.id, - value: getUserInput(field.name, field.fieldType), - })); +```typescript +if (options.collectData?.url) { + // Use the "required" list from options.collectData.schema to determine which fields to prefill + const prefillData = { + fullName: "John Doe", + dateOfBirth: "1990-01-15", + pobAddress: "123 Main St, New York, NY 10001", + }; + const prefillBase64 = btoa(JSON.stringify(prefillData)); + const separator = options.collectData.url.includes("?") ? "&" : "?"; + const webViewUrl = `${options.collectData.url}${separator}prefill=${prefillBase64}`; + + // Show WebView — see WebView Implementation section below + showDataCollectionWebView(webViewUrl); } ``` + + @@ -252,6 +268,89 @@ if (result.status === "succeeded") { +## WebView Implementation + +When `collectData.url` is present, display the URL in a WebView using `react-native-webview`. Install the dependency: + +```bash +npm install react-native-webview@13.16.0 +``` + +```tsx +import React, { useCallback } from "react"; +import { WebView, WebViewMessageEvent } from "react-native-webview"; +import { Linking, View, ActivityIndicator } from "react-native"; + +interface PayDataCollectionWebViewProps { + url: string; + onComplete: () => void; + onError: (error: string) => void; +} + +function PayDataCollectionWebView({ + url, + onComplete, + onError, +}: PayDataCollectionWebViewProps) { + const handleMessage = useCallback( + (event: WebViewMessageEvent) => { + try { + const data = JSON.parse(event.nativeEvent.data); + switch (data.type) { + case "IC_COMPLETE": + onComplete(); + break; + case "IC_ERROR": + onError(data.error || "Unknown error"); + break; + } + } catch { + // Ignore non-JSON messages + } + }, + [onComplete, onError] + ); + + const handleNavigationRequest = useCallback( + (request: { url: string }) => { + // Open external links (T&C, Privacy Policy) in system browser + if (!request.url.includes("pay.walletconnect.com")) { + Linking.openURL(request.url); + return false; + } + return true; + }, + [] + ); + + return ( + ( + + + + )} + /> + ); +} + +function buildPrefillUrl( + baseUrl: string, + prefillData: Record +): string { + if (Object.keys(prefillData).length === 0) return baseUrl; + const base64 = btoa(JSON.stringify(prefillData)); + const separator = baseUrl.includes("?") ? "&" : "?"; + return `${baseUrl}${separator}prefill=${base64}`; +} +``` + ## Complete Example Here's a complete implementation example: @@ -301,10 +400,10 @@ class PaymentManager { ) ); - // Step 5: Collect user data if required - let collectedData: CollectDataFieldResult[] | undefined; - if (options.collectData) { - collectedData = await this.collectUserData(options.collectData.fields); + // Step 5: Collect data via WebView if required + if (options.collectData?.url) { + // Show WebView and wait for IC_COMPLETE + await this.showDataCollectionWebView(options.collectData.url); } // Step 6: Confirm payment @@ -312,7 +411,6 @@ class PaymentManager { paymentId: options.paymentId, optionId: selectedOption.id, signatures, - collectedData, }); return result; @@ -324,18 +422,10 @@ class PaymentManager { private async signAction(action: Action, walletAddress: string): Promise { const { chainId, method, params } = action.walletRpc; - + // Use your wallet's signing implementation return await wallet.signTypedData(chainId, JSON.parse(params)); } - - private async collectUserData(fields: CollectDataField[]): Promise { - // Implement your UI to collect user data - return fields.map((field) => ({ - id: field.id, - value: getUserInput(field.name, field.fieldType), - })); - } } ``` @@ -494,8 +584,6 @@ interface ConfirmPaymentParams { optionId: string; /** Signatures from wallet RPC calls */ signatures: string[]; - /** Collected data fields (if required) */ - collectedData?: CollectDataFieldResult[]; } ``` @@ -511,6 +599,15 @@ interface PaymentOptionsResponse { options: PaymentOption[]; /** Data collection requirements (if any) */ collectData?: CollectDataAction; + /** Transaction result details (present when payment already completed) */ + resultInfo?: PaymentResultInfo; +} + +interface PaymentResultInfo { + /** Transaction ID */ + txId: string; + /** Token amount details */ + optionAmount: PayAmount; } interface ConfirmPaymentResponse { @@ -618,23 +715,10 @@ interface BuyerInfo { ```typescript interface CollectDataAction { - fields: CollectDataField[]; -} - -interface CollectDataField { - /** ID of the field for submission */ - id: string; - /** Human readable name of the field */ - name: string; - /** Whether the field is required */ - required: boolean; - /** Type of the field */ - fieldType: CollectDataFieldType; -} - -interface CollectDataFieldResult { - id: string; - value: string; + /** WebView URL for data collection */ + url: string; + /** JSON schema describing required fields */ + schema?: string; } ``` @@ -655,3 +739,5 @@ interface CollectDataFieldResult { 7. **Expiration**: Check `paymentInfo.expiresAt` and warn users if time is running low 8. **User Data**: Only collect data when `collectData` is present in the response and you don't already have the required user data. If you already have the required data, you can submit this without collecting from the user. You must make sure the user accepts WalletConnect Terms and Conditions and Privacy Policy before submitting user information to WalletConnect. + +9. **WebView Data Collection**: When `collectData.url` is present, display the URL in a WebView using `react-native-webview` rather than building native forms. The WebView handles form rendering, validation, and T&C acceptance. diff --git a/payments/wallets/standalone/swift.mdx b/payments/wallets/standalone/swift.mdx index 20fae7e..867293e 100644 --- a/payments/wallets/standalone/swift.mdx +++ b/payments/wallets/standalone/swift.mdx @@ -4,6 +4,10 @@ description: "Integrate WalletConnect Pay into your iOS wallet to enable seamles sidebarTitle: "Swift" --- +import WebViewOverview from "/snippets/webview-data-collection-overview.mdx"; +import WebViewBestPractices from "/snippets/webview-data-collection-best-practices.mdx"; +import WebViewMessageTypes from "/snippets/webview-message-types.mdx"; + The WalletConnect Pay SDK allows wallet users to pay merchants using their crypto assets. The SDK handles payment option discovery, permit signing coordination, and payment confirmation while leveraging your wallet's existing signing infrastructure. ## Requirements @@ -119,6 +123,7 @@ sequenceDiagram participant Wallet participant PaySDK as Pay SDK participant Backend as WalletConnect Pay + participant WebView User->>Wallet: Scan QR / Open payment link Wallet->>PaySDK: getPaymentOptions(link, accounts) @@ -126,21 +131,23 @@ sequenceDiagram Backend-->>PaySDK: Payment options + merchant info PaySDK-->>Wallet: PaymentOptionsResponse Wallet->>User: Display payment options - + User->>Wallet: Select payment option Wallet->>PaySDK: getRequiredPaymentActions(paymentId, optionId) PaySDK->>Backend: Get signing actions Backend-->>PaySDK: Required wallet RPC actions PaySDK-->>Wallet: List of actions to sign - + Wallet->>User: Request signature(s) User->>Wallet: Approve & sign - + alt Data collection required - Wallet->>User: Request additional info - User->>Wallet: Provide data + Wallet->>WebView: Load collectDataAction.url in WebView + WebView->>User: Display data collection form + User->>WebView: Fill form & accept T&C + WebView-->>Wallet: IC_COMPLETE message end - + Wallet->>PaySDK: confirmPayment(signatures, collectedData) PaySDK->>Backend: Submit payment Backend-->>PaySDK: Payment status @@ -250,34 +257,32 @@ Signatures must be in the same order as the actions array. If `response.collectData` is not nil, you must collect user information before confirming: -```swift -var collectedData: [CollectDataFieldResult]? = nil + -if let collectDataAction = response.collectData { - collectedData = [] - - for field in collectDataAction.fields { - // Show appropriate UI based on field.fieldType - let value: String - - switch field.fieldType { - case .text: - // Show text input for name fields - value = userInputtedValue - case .date: - // Show date picker for date of birth - // Format: "YYYY-MM-DD" - value = "1990-01-15" - } - - collectedData?.append(CollectDataFieldResult( - id: field.id, - value: value - )) - } +```swift +if let collectData = response.collectData, let url = collectData.url { + // Build prefill URL with known user data + // Use the "required" list from collectData.schema to determine which fields to prefill + let prefillData: [String: String] = [ + "fullName": "John Doe", + "dateOfBirth": "1990-01-15", + "pobAddress": "123 Main St, New York, NY 10001" + ] + let jsonData = try JSONSerialization.data(withJSONObject: prefillData) + let prefillBase64 = jsonData.base64EncodedString() + var components = URLComponents(string: url)! + var queryItems = components.queryItems ?? [] + queryItems.append(URLQueryItem(name: "prefill", value: prefillBase64)) + components.queryItems = queryItems + let webViewUrl = components.string! + + // Show WebView — see WebView Implementation section below + showWebView(url: webViewUrl) } ``` + + @@ -311,6 +316,103 @@ case .requiresAction: +## WebView Implementation + +When `collectDataAction.url` is present, display the URL in a `WKWebView`. The WebView handles form rendering, validation, and T&C acceptance. + +```swift +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 + } + // Open external links (T&C, Privacy Policy) in Safari + if let host = url.host, !host.contains("pay.walletconnect.com") { + UIApplication.shared.open(url) + decisionHandler(.cancel) + return + } + decisionHandler(.allow) + } + } +} + +func buildPrefillUrl(baseUrl: String, prefillData: [String: String]) -> String { + guard !prefillData.isEmpty, + let jsonData = try? JSONSerialization.data(withJSONObject: prefillData) else { + return baseUrl + } + let base64 = jsonData.base64EncodedString() + var components = URLComponents(string: baseUrl)! + var queryItems = components.queryItems ?? [] + queryItems.append(URLQueryItem(name: "prefill", value: base64)) + components.queryItems = queryItems + return components.string ?? baseUrl +} +``` + ## Complete Example Here's a complete implementation example: @@ -362,18 +464,17 @@ class PaymentManager { signatures.append(signature) } - // 5. Collect user data if required - var collectedData: [CollectDataFieldResult]? = nil - if let collectData = optionsResponse.collectData { - collectedData = try await collectUserData(fields: collectData.fields) + // 5. Collect data via WebView if required + if let collectData = optionsResponse.collectData, let url = collectData.url { + // Show WebView and wait for IC_COMPLETE message + try await showDataCollectionWebView(url: url) } - + // 6. Confirm payment let result = try await WalletConnectPay.instance.confirmPayment( paymentId: optionsResponse.paymentId, optionId: selectedOption.id, - signatures: signatures, - collectedData: collectedData + signatures: signatures ) guard result.status == .succeeded else { @@ -387,7 +488,7 @@ class PaymentManager { signer: YourSignerProtocol ) async throws -> String { let rpc = action.walletRpc - + // Parse params: ["address", "typedDataJson"] guard let paramsData = rpc.params.data(using: .utf8), let params = try JSONSerialization.jsonObject(with: paramsData) as? [Any], @@ -395,26 +496,13 @@ class PaymentManager { let typedDataJson = params[1] as? String else { throw PaymentError.invalidParams } - + // Use your wallet's signing implementation return try await signer.signTypedData( data: typedDataJson, address: walletAddress ) } - - private func collectUserData( - fields: [CollectDataField] - ) async throws -> [CollectDataFieldResult] { - // Implement your UI to collect user data - // This is typically done via a form/modal - return fields.map { field in - CollectDataFieldResult( - id: field.id, - value: getUserInput(for: field) - ) - } - } } ``` @@ -501,7 +589,7 @@ Main client for payment operations. |--------|-------------| | `getPaymentOptions(paymentLink:accounts:includePaymentInfo:)` | Fetch available payment options | | `getRequiredPaymentActions(paymentId:optionId:)` | Get signing actions for a payment option | -| `confirmPayment(paymentId:optionId:signatures:collectedData:maxPollMs:)` | Confirm and execute the payment | +| `confirmPayment(paymentId:optionId:signatures:maxPollMs:)` | Confirm and execute the payment | ### Data Types @@ -513,6 +601,12 @@ struct PaymentOptionsResponse { let info: PaymentInfo? // Merchant and amount details let options: [PaymentOption] // Available payment methods let collectData: CollectDataAction? // Required user data fields (travel rule) + let resultInfo: PaymentResultInfo? // Transaction result details (present when payment already completed) +} + +struct PaymentResultInfo { + let txId: String // Transaction ID + let optionAmount: PayAmount // Token amount details } ``` @@ -575,24 +669,8 @@ struct WalletRpcAction { ```swift struct CollectDataAction { - let fields: [CollectDataField] // Required fields to collect -} - -struct CollectDataField { - let id: String // Field identifier - let name: String // Display name - let required: Bool // Whether field is required - let fieldType: CollectDataFieldType // Type of input needed -} - -enum CollectDataFieldType { - case text // Text input (names, etc.) - case date // Date input (format: YYYY-MM-DD) -} - -struct CollectDataFieldResult { - let id: String // Field identifier (from CollectDataField) - let value: String // User-provided value + let url: String // WebView URL for data collection + let schema: String? // JSON schema describing required fields } ``` @@ -629,3 +707,5 @@ enum PaymentStatus { 6. **Expiration**: Check `paymentInfo.expiresAt` and warn users if time is running low 7. **User Data**: Only collect data when `collectData` is present in the response and you don't already have the required user data. If you already have the required data, you can submit this without collecting from the user. You must make sure the user accepts WalletConnect Terms and Conditions and Privacy Policy before submitting user information to WalletConnect. + +8. **WebView Data Collection**: When `collectData.url` is present, display the URL in a WKWebView rather than building native forms. The WebView handles form rendering, validation, and T&C acceptance. diff --git a/payments/wallets/walletkit/ai-prompts/flutter.mdx b/payments/wallets/walletkit/ai-prompts/flutter.mdx index 6440d42..a6300e3 100644 --- a/payments/wallets/walletkit/ai-prompts/flutter.mdx +++ b/payments/wallets/walletkit/ai-prompts/flutter.mdx @@ -39,12 +39,12 @@ Before starting, ensure your wallet app has: ### Payment Flow Overview ``` -Payment Link → Detect → Get Options → [Collect Data] → Sign Actions → Confirm Payment → Result +Payment Link → Detect → Get Options → [WebView Data Collection] → Sign Actions → Confirm Payment → Result ``` 1. **Payment Link Detection**: Identify incoming payment links from QR codes, deep links, or text input 2. **Get Payment Options**: Retrieve available payment methods with merchant information -3. **Data Collection** (optional): Collect KYC/compliance data if required by the payment +3. **WebView Data Collection** (optional): Display WebView form for KYC/compliance data if required by the payment 4. **Sign Actions**: Execute wallet signing operations (typically `eth_signTypedData_v4`) 5. **Confirm Payment**: Submit signatures to complete the transaction 6. **Handle Result**: Display success/failure and handle polling if needed @@ -57,6 +57,7 @@ Payment Link → Detect → Get Options → [Collect Data] → Sign Actions → | `PaymentOptionsResponse` | Contains payment ID, options, merchant info, and data collection requirements | | `PaymentOption` | Individual payment option with amount, account, and actions | | `Action` / `WalletRpcAction` | Signing request with chain ID, method, and parameters | +| `CollectDataAction` | Optional data collection with WebView URL | | `ConfirmPaymentRequest` | Request to confirm payment with signatures | | `ConfirmPaymentResponse` | Payment status and polling information | | `PaymentStatus` | Enum: `requires_action`, `processing`, `succeeded`, `failed`, `expired` | @@ -194,7 +195,18 @@ class PaymentOptionsResponse { final String paymentId; // Unique ID for this payment session final PaymentInfo? info; // Merchant and payment details final List options; // Available payment methods - final CollectDataAction? collectData; // Optional data collection requirements + final CollectDataAction? collectData; // Optional data collection with WebView URL + final PaymentResultInfo? resultInfo; // Transaction result details (present when payment already completed) +} + +class PaymentResultInfo { + final String txId; // Transaction ID + final PayAmount optionAmount; // Token amount details +} + +class CollectDataAction { + final String url; // WebView URL for data collection + final String? schema; // JSON schema describing required fields } class PaymentInfo { @@ -214,57 +226,121 @@ class PaymentOption { } ``` -### Step 5: Handle Data Collection (Optional) +### Step 5: Handle Data Collection via WebView -If `response.collectData` is not null, you must collect the required data before proceeding. +If `response.collectData` is not null and has a `url`, display the URL in a WebView for data collection. The hosted form handles rendering, validation, and T&C acceptance. ```dart -/// Collect required data fields -Future?> collectRequiredData( - CollectDataAction collectData, +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// Show WebView for data collection +Future showDataCollectionWebView( + BuildContext context, + String url, ) async { - final List results = []; + final completer = Completer(); + + Navigator.push(context, MaterialPageRoute( + builder: (_) => PayDataCollectionWebView( + url: url, + onComplete: () { + Navigator.pop(context); + completer.complete(true); + }, + onError: (error) { + Navigator.pop(context); + completer.complete(false); + showError('Data collection failed: $error'); + }, + ), + )); - for (final field in collectData.fields) { - // Present UI to collect each field - final value = await showDataCollectionDialog(field); + return completer.future; +} - if (value == null && field.required) { - // User cancelled and field is required - return null; - } +class PayDataCollectionWebView extends StatefulWidget { + final String url; + final VoidCallback onComplete; + final ValueChanged onError; - if (value != null) { - results.add(CollectDataFieldResult( - id: field.id, - value: value, - )); - } - } + const PayDataCollectionWebView({ + super.key, + required this.url, + required this.onComplete, + required this.onError, + }); - return results; + @override + State createState() => + _PayDataCollectionWebViewState(); } -/// Example: Show dialog for a single field -Future showDataCollectionDialog(CollectDataField field) async { - switch (field.fieldType) { - case CollectDataFieldType.text: - return await showTextInputDialog( - label: field.name, - required: field.required, - ); - case CollectDataFieldType.date: - return await showDatePickerDialog( - label: field.name, - required: field.required, - ); +class _PayDataCollectionWebViewState extends State { + late final WebViewController _controller; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => setState(() => _isLoading = false), + onNavigationRequest: (request) { + if (!request.url.contains('pay.walletconnect.com')) { + launchUrl(Uri.parse(request.url), + mode: LaunchMode.externalApplication); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + )) + ..addJavaScriptChannel( + 'ReactNativeWebView', + onMessageReceived: (message) { + try { + final data = jsonDecode(message.message) as Map; + switch (data['type']) { + case 'IC_COMPLETE': + widget.onComplete(); + break; + case 'IC_ERROR': + widget.onError(data['error'] ?? 'Unknown error'); + break; + } + } catch (_) {} + }, + ) + ..loadRequest(Uri.parse(widget.url)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Information Collection')), + body: Stack( + children: [ + WebViewWidget(controller: _controller), + if (_isLoading) const Center(child: CircularProgressIndicator()), + ], + ), + ); } } ``` -**Field Types:** -- `CollectDataFieldType.text` - Free-form text input (e.g., full name, place of birth) -- `CollectDataFieldType.date` - Date input, format as `YYYY-MM-DD` +> **Important:** When using the WebView approach, do **not** pass `collectedData` to `confirmPayment()`. The WebView submits data directly to the backend. + +Add `webview_flutter` and `url_launcher` to your dependencies: + +```yaml +dependencies: + webview_flutter: ^4.10.0 + url_launcher: ^6.1.0 +``` ### Step 6: Get Required Payment Actions @@ -428,13 +504,11 @@ Future confirmPayment({ required String paymentId, required String optionId, required List signatures, - List? collectedData, }) async { final request = ConfirmPaymentRequest( paymentId: paymentId, optionId: optionId, signatures: signatures, - collectedData: collectedData, maxPollMs: 60000, // Poll for up to 60 seconds ); @@ -513,11 +587,13 @@ Future processPayment(String paymentLink) async { final selectedOption = await showPaymentOptionsModal(options); if (selectedOption == null) return; // User cancelled - // Step 3: Collect data if required - List? collectedData; - if (options.collectData != null) { - collectedData = await collectRequiredData(options.collectData!); - if (collectedData == null) return; // User cancelled + // Step 3: Collect data via WebView if required + if (options.collectData?.url != null) { + final success = await showDataCollectionWebView( + context, + options.collectData!.url, + ); + if (!success) return; // User cancelled or error } // Step 4: Get actions if not included in option @@ -540,7 +616,6 @@ Future processPayment(String paymentLink) async { paymentId: options.paymentId, optionId: selectedOption.id, signatures: signatures, - collectedData: collectedData, maxPollMs: 60000, ), ); @@ -856,7 +931,7 @@ walletKit.pay - [ ] `eth_signTypedData_v4` signing implemented - [ ] Hex value normalization for typed data signing - [ ] Signature order matches action order -- [ ] Data collection UI for compliance fields +- [ ] WebView data collection for compliance (using webview_flutter) - [ ] Error handling for all error types - [ ] Loading states during API calls - [ ] Success/failure result screens diff --git a/payments/wallets/walletkit/ai-prompts/kotlin.mdx b/payments/wallets/walletkit/ai-prompts/kotlin.mdx index 226e96b..cebe31c 100644 --- a/payments/wallets/walletkit/ai-prompts/kotlin.mdx +++ b/payments/wallets/walletkit/ai-prompts/kotlin.mdx @@ -44,9 +44,9 @@ dependencies { WalletConnect Pay enables crypto payments through payment links. The flow works as follows: ``` -Payment Link → Get Options → [Collect Data] → Sign Actions → Confirm Payment - ↓ ↓ ↓ ↓ ↓ - Detection API call Optional KYC Wallet signs Backend confirms +Payment Link → Get Options → [WebView Data Collection] → Sign Actions → Confirm Payment + ↓ ↓ ↓ ↓ ↓ + Detection API call Optional KYC Wallet signs Backend confirms ``` ### WalletKit Integration @@ -161,12 +161,16 @@ data class PaymentOption( val estimatedTxs: Int? ) -// Data collection field (for KYC/compliance) -data class CollectDataField( - val id: String, - val name: String, - val fieldType: CollectDataFieldType, // TEXT or DATE - val required: Boolean +// Data collection action (for KYC/compliance via WebView) +data class CollectDataAction( + val url: String, // WebView URL for data collection + val schema: String? // JSON schema describing required fields +) + +// Transaction result details (present when payment already completed) +data class PaymentResultInfo( + val txId: String, // Transaction ID + val optionAmount: PaymentAmount // Token amount details ) // Signing action required from wallet @@ -206,39 +210,99 @@ suspend fun getPaymentOptions( } ``` -#### 2.2 Handle Data Collection (if required) +#### 2.2 Handle Data Collection via WebView (if required) Some payments require user information (KYC/AML compliance). Check for `collectDataAction` in the response: ```kotlin fun processPaymentOptionsResponse(response: Wallet.Model.PaymentOptionsResponse) { - val collectDataFields = response.collectDataAction?.fields ?: emptyList() + val collectDataUrl = response.collectDataAction?.url - if (collectDataFields.isNotEmpty()) { - // Show data collection UI before payment options - showDataCollectionFlow(collectDataFields) + if (collectDataUrl != null) { + // Show WebView for data collection before payment options + showDataCollectionWebView(collectDataUrl) } else { // Show payment options directly showPaymentOptions(response.options, response.info) } } +``` -// Collect data from user for each field -data class CollectedData( - val fieldId: String, - val value: String -) +When `collectDataAction.url` is present, display the URL in a WebView. The hosted form handles rendering, validation, and Terms & Conditions acceptance. The WebView communicates completion via JavaScript bridge messages (`IC_COMPLETE` / `IC_ERROR`). -fun collectUserData(fields: List): List { - // Implement UI to collect data based on field type: - // - TEXT: Free-form text input - // - DATE: Date picker (format: YYYY-MM-DD) - return fields.map { field -> - CollectedData( - fieldId = field.id, - value = getUserInputForField(field) - ) - } +> **Important:** When using the WebView approach, do **not** pass `collectedData` to `confirmPayment()`. The WebView submits data directly to the backend. + +#### WebView Implementation + +```kotlin +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient +import android.webkit.WebResourceRequest +import android.content.Intent +import android.net.Uri +import android.util.Base64 +import androidx.compose.runtime.Composable +import androidx.compose.ui.viewinterop.AndroidView +import org.json.JSONObject + +@Composable +fun PayDataCollectionWebView( + url: String, + onComplete: () -> Unit, + onError: (String) -> Unit +) { + AndroidView(factory = { context -> + WebView(context).apply { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.allowFileAccess = false + + addJavascriptInterface( + object { + @JavascriptInterface + fun onDataCollectionComplete(json: String) { + val message = JSONObject(json) + when (message.optString("type")) { + "IC_COMPLETE" -> onComplete() + "IC_ERROR" -> onError( + message.optString("error", "Unknown error") + ) + } + } + }, + "AndroidWallet" + ) + + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val requestUrl = request?.url?.toString() ?: return false + if (!requestUrl.contains("pay.walletconnect.com")) { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(requestUrl))) + return true + } + return false + } + } + + loadUrl(url) + } + }) +} + +fun buildPrefillUrl(baseUrl: String, prefillData: Map): String { + if (prefillData.isEmpty()) return baseUrl + val json = JSONObject(prefillData).toString() + val base64 = Base64.encodeToString( + json.toByteArray(), + Base64.NO_WRAP or Base64.URL_SAFE + ) + return Uri.parse(baseUrl).buildUpon() + .appendQueryParameter("prefill", base64) + .build().toString() } ``` @@ -361,22 +425,13 @@ Submit signatures and collected data to confirm the payment: suspend fun confirmPayment( paymentId: String, optionId: String, - signatures: List, - collectedData: List? + signatures: List ): Result { - val fieldResults = collectedData?.map { data -> - Wallet.Model.CollectDataFieldResult( - id = data.fieldId, - value = data.value - ) - } - return WalletKit.Pay.confirmPayment( Wallet.Params.ConfirmPayment( paymentId = paymentId, optionId = optionId, - signatures = signatures, - collectedData = fieldResults + signatures = signatures ) ) } @@ -421,11 +476,8 @@ sealed class PaymentUiState { val hasDataCollection: Boolean ) : PaymentUiState() - data class CollectingData( - val currentStepIndex: Int, - val totalSteps: Int, - val currentField: Wallet.Model.CollectDataField, - val currentValue: String + data class WebViewDataCollection( + val url: String ) : PaymentUiState() data class Options( @@ -467,8 +519,7 @@ class PaymentViewModel( private var currentPaymentId: String? = null private var selectedOptionId: String? = null private var pendingActions: List = emptyList() - private var collectedData: MutableMap = mutableMapOf() - private var collectDataFields: List = emptyList() + private var webViewUrl: String? = null private var storedPaymentInfo: Wallet.Model.PaymentInfo? = null private var storedOptions: List = emptyList() @@ -483,11 +534,11 @@ class PaymentViewModel( currentPaymentId = response.paymentId storedPaymentInfo = response.info storedOptions = response.options - collectDataFields = response.collectDataAction?.fields ?: emptyList() + webViewUrl = response.collectDataAction?.url if (response.options.isEmpty()) { _uiState.value = PaymentUiState.Error("No payment options available") - } else if (collectDataFields.isNotEmpty()) { + } else if (webViewUrl != null) { _uiState.value = PaymentUiState.Intro( paymentInfo = response.info, hasDataCollection = true @@ -540,20 +591,12 @@ class PaymentViewModel( walletRepository.signWalletRpcAction(action.action) } - // Convert collected data - val fieldResults = if (collectedData.isNotEmpty()) { - collectedData.map { (id, value) -> - Wallet.Model.CollectDataFieldResult(id = id, value = value) - } - } else null - - // Confirm payment + // Confirm payment (no collectedData - WebView handles submission) WalletKit.Pay.confirmPayment( Wallet.Params.ConfirmPayment( paymentId = paymentId, optionId = optionId, - signatures = signatures, - collectedData = fieldResults + signatures = signatures ) ) .onSuccess { response -> @@ -579,16 +622,23 @@ class PaymentViewModel( } } - fun submitFieldValue(fieldId: String, value: String) { - collectedData[fieldId] = value - // Navigate to next field or options screen + fun onWebViewComplete() { + // WebView data collection completed, proceed to options + _uiState.value = PaymentUiState.Options( + paymentInfo = storedPaymentInfo, + options = storedOptions + ) + } + + fun onWebViewError(error: String) { + _uiState.value = PaymentUiState.Error(error) } fun cancel() { currentPaymentId = null selectedOptionId = null pendingActions = emptyList() - collectedData.clear() + webViewUrl = null _uiState.value = PaymentUiState.Loading } } @@ -626,12 +676,10 @@ fun PaymentScreen( onCancel = { viewModel.cancel(); onDismiss() } ) - is PaymentUiState.CollectingData -> DataCollectionContent( - field = state.currentField, - currentValue = state.currentValue, - stepInfo = "${state.currentStepIndex + 1}/${state.totalSteps}", - onSubmit = { value -> viewModel.submitFieldValue(state.currentField.id, value) }, - onBack = { viewModel.goBack() } + is PaymentUiState.WebViewDataCollection -> PayDataCollectionWebView( + url = state.url, + onComplete = { viewModel.onWebViewComplete() }, + onError = { error -> viewModel.onWebViewError(error) } ) is PaymentUiState.Options -> OptionsContent( @@ -824,9 +872,10 @@ class PaymentFlowExample( val paymentId = optionsResponse.paymentId - // 3. Handle data collection if required - val collectedData = optionsResponse.collectDataAction?.fields?.let { fields -> - collectDataFromUser(fields) + // 3. Handle data collection via WebView if required + optionsResponse.collectDataAction?.url?.let { url -> + showDataCollectionWebView(url) + // Wait for IC_COMPLETE before proceeding } // 4. Let user select payment option @@ -847,10 +896,7 @@ class PaymentFlowExample( Wallet.Params.ConfirmPayment( paymentId = paymentId, optionId = selectedOption.id, - signatures = signatures, - collectedData = collectedData?.map { - Wallet.Model.CollectDataFieldResult(it.fieldId, it.value) - } + signatures = signatures ) ).getOrThrow() diff --git a/payments/wallets/walletkit/ai-prompts/react-native.mdx b/payments/wallets/walletkit/ai-prompts/react-native.mdx index fdada2b..6d2c51e 100644 --- a/payments/wallets/walletkit/ai-prompts/react-native.mdx +++ b/payments/wallets/walletkit/ai-prompts/react-native.mdx @@ -259,12 +259,11 @@ const confirmPayment = async () => { } } - // Confirm the payment + // Confirm the payment (no collectedData - WebView handles submission) const result = await payClient.confirmPayment({ paymentId: paymentData.paymentId, optionId: selectedOption.id, signatures, - collectedData: collectedDataResults.length > 0 ? collectedDataResults : undefined, }); return result; @@ -338,14 +337,14 @@ If you must parse the typed data, be aware of these issues: ``` LOADING → ERROR (if failed) - → COLLECT_DATA (if collectData exists) → CONFIRM → CONFIRMING → SUCCESS + → WEBVIEW_DATA_COLLECTION (if collectData.url exists) → CONFIRM → CONFIRMING → SUCCESS → CONFIRM → CONFIRMING → SUCCESS ``` ### State Definition ```typescript -type Step = 'loading' | 'intro' | 'collectData' | 'confirm' | 'confirming' | 'result'; +type Step = 'loading' | 'intro' | 'webviewDataCollection' | 'confirm' | 'confirming' | 'result'; interface PaymentModalState { step: Step; @@ -355,8 +354,7 @@ interface PaymentModalState { paymentActions: Action[] | null; isLoadingActions: boolean; actionsError: string | null; - collectedData: Record; - fieldErrors: Record; + webViewUrl: string | null; } ``` @@ -364,7 +362,7 @@ interface PaymentModalState { 1. **Loading View** - Spinner with message 2. **Intro View** - Merchant info, amount, continue button -3. **Collect Data View** - Form fields (if `paymentOptions.collectData` exists) +3. **WebView Data Collection View** - WebView form (if `paymentOptions.collectData?.url` exists) 4. **Confirm View** - Payment options list, selected option details, approve button 5. **Result View** - Success or error message with close button @@ -457,7 +455,6 @@ await walletKit.pay.confirmPayment({ paymentId: string, optionId: string, signatures: string[], // Array of 0x-prefixed signatures - collectedData?: { id, value }[], // If collectData was required }); ``` @@ -476,6 +473,12 @@ interface PaymentOptionsResponse { options: PaymentOption[]; collectData?: CollectDataAction; info?: PaymentInfo; + resultInfo?: PaymentResultInfo; // present when payment already completed +} + +interface PaymentResultInfo { + txId: string; + optionAmount: PayAmount; } interface PaymentOption { @@ -498,14 +501,8 @@ interface PayAmount { } interface CollectDataAction { - fields: CollectDataField[]; -} - -interface CollectDataField { - id: string; - name: string; - required: boolean; - fieldType: 'text' | 'date'; + url: string; // WebView URL for data collection + schema?: string; // JSON schema describing required fields } interface Action { @@ -574,6 +571,79 @@ const signature = await wallet.signTypedData({ domain, types, primaryType, messa const signature = await web3.eth.signTypedData(address, typedData); ``` +### 3.5 WebView Data Collection + +When `paymentOptions.collectData?.url` is present, display the URL in a WebView. The hosted form handles rendering, validation, and T&C acceptance. + +Install `react-native-webview`: + +```bash +npm install react-native-webview@13.16.0 +``` + +```tsx +import React, { useCallback } from "react"; +import { WebView, WebViewMessageEvent } from "react-native-webview"; +import { Linking, View, ActivityIndicator } from "react-native"; + +interface PayDataCollectionWebViewProps { + url: string; + onComplete: () => void; + onError: (error: string) => void; +} + +function PayDataCollectionWebView({ + url, + onComplete, + onError, +}: PayDataCollectionWebViewProps) { + const handleMessage = useCallback( + (event: WebViewMessageEvent) => { + try { + const data = JSON.parse(event.nativeEvent.data); + switch (data.type) { + case "IC_COMPLETE": + onComplete(); + break; + case "IC_ERROR": + onError(data.error || "Unknown error"); + break; + } + } catch { + // Ignore non-JSON messages + } + }, + [onComplete, onError] + ); + + const handleNavigationRequest = useCallback((request: { url: string }) => { + if (!request.url.includes("pay.walletconnect.com")) { + Linking.openURL(request.url); + return false; + } + return true; + }, []); + + return ( + ( + + + + )} + /> + ); +} +``` + +> **Important:** When using the WebView approach, do **not** pass `collectedData` to `confirmPayment()`. The WebView submits data directly to the backend. + --- ## Expo Considerations @@ -660,6 +730,7 @@ When implementation is complete, verify: - [ ] Payment sub-components (IntroView, ConfirmView, etc.) - [ ] Payment utility functions - [ ] Payment state reducer (if using reducer pattern) +- [ ] WebView data collection component (using react-native-webview) **Native Build:** diff --git a/payments/wallets/walletkit/ai-prompts/swift.mdx b/payments/wallets/walletkit/ai-prompts/swift.mdx index 11ffb0d..6e9e3b1 100644 --- a/payments/wallets/walletkit/ai-prompts/swift.mdx +++ b/payments/wallets/walletkit/ai-prompts/swift.mdx @@ -127,12 +127,12 @@ dependencies: [ ### State Machine ``` -[Intro] ──▶ [Name Input]* ──▶ [Date of Birth]* ──▶ [Confirmation] ──▶ [Confirming] ──▶ [Success] - │ - ▼ - [Error] +[Intro] ──▶ [WebView Data Collection]* ──▶ [Confirmation] ──▶ [Confirming] ──▶ [Success] + │ + ▼ + [Error] -* Travel rule steps - skip if collectData is nil +* WebView step - skip if collectData.url is nil ``` --- @@ -327,9 +327,9 @@ func loadPaymentOptions(paymentLink: String, walletAddress: String) async throws print("Amount: \(info.amount.display.assetSymbol) \(info.amount.value)") } - // Check if travel rule data collection is required - if let collectData = response.collectData { - print("Travel rule data required: \(collectData.fields.count) fields") + // Check if data collection is required (via WebView) + if let collectData = response.collectData, let url = collectData.url { + print("Data collection required via WebView: \(url)") } return response @@ -377,15 +377,10 @@ func confirmPayment( signatures.append(signature) } - // 3. Collect user data if required (travel rule) - var collectedData: [CollectDataFieldResult]? = nil - if let collectDataAction = response.collectData { - collectedData = collectDataAction.fields.map { field in - CollectDataFieldResult( - id: field.id, - value: resolveFieldValue(for: field) - ) - } + // 3. Collect data via WebView if required + if let collectData = response.collectData, let url = collectData.url { + // Show WebView and wait for IC_COMPLETE message + try await showDataCollectionWebView(url: url) } // 4. Confirm payment @@ -393,7 +388,6 @@ func confirmPayment( paymentId: paymentId, optionId: selectedOption.id, signatures: signatures, - collectedData: collectedData, maxPollMs: 60000 ) @@ -410,30 +404,96 @@ func confirmPayment( 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. + +```swift +import WebKit +import SwiftUI -// Map collected user data to field values -private func resolveFieldValue(for field: CollectDataField) -> String { - let fieldId = field.id.lowercased() - let fieldName = field.name.lowercased() - - if fieldId.contains("fullname") || fieldId.contains("full_name") || - fieldName.contains("full name") { - return "\(firstName) \(lastName)" - } else if fieldId.contains("firstname") || fieldName.contains("first") { - return firstName - } else if fieldId.contains("lastname") || fieldName.contains("last") { - return lastName - } else if fieldId.contains("dob") || fieldId.contains("birth") { - // Format: YYYY-MM-DD - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - return formatter.string(from: dateOfBirth) +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) + } } - return "" } ``` -**CRITICAL**: Signatures must be in the same order as the actions array. +> **Important:** When using the WebView approach, do **not** pass `collectedData` to `confirmPayment()`. The WebView submits data directly to the backend. --- @@ -463,7 +523,6 @@ WalletKit.instance.Pay.confirmPayment( paymentId: String, optionId: String, signatures: [String], - collectedData: [CollectDataFieldResult]? = nil, maxPollMs: Int64? = nil ) async throws -> ConfirmPaymentResultResponse ``` @@ -476,6 +535,12 @@ struct PaymentOptionsResponse { 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 { @@ -516,19 +581,8 @@ struct WalletRpcAction { } struct CollectDataAction { - let fields: [CollectDataField] -} - -struct CollectDataField { - let id: String - let name: String - let required: Bool - let fieldType: CollectDataFieldType // .text or .date -} - -struct CollectDataFieldResult { - let id: String - let value: String // For dates: "YYYY-MM-DD" + let url: String // WebView URL for data collection + let schema: String? // JSON schema describing required fields } struct ConfirmPaymentResultResponse { @@ -599,7 +653,7 @@ enum PaymentStatus { 2. **Wrong signature order** - Signatures array must match actions array order exactly. -3. **Skipping travel rule check** - Always check `response.collectData` before confirmation. +3. **Skipping WebView data collection** - Always check `response.collectData?.url` and show the WebView before confirmation. 4. **Not handling cold start** - Deep links on app launch need delayed handling. diff --git a/payments/wallets/walletkit/flutter.mdx b/payments/wallets/walletkit/flutter.mdx index 7625d9c..8222e83 100644 --- a/payments/wallets/walletkit/flutter.mdx +++ b/payments/wallets/walletkit/flutter.mdx @@ -5,6 +5,9 @@ sidebarTitle: "Flutter" --- import AppIdSnippet from "/snippets/app-id.mdx"; +import WebViewOverview from "/snippets/webview-data-collection-overview.mdx"; +import WebViewBestPractices from "/snippets/webview-data-collection-best-practices.mdx"; +import WebViewMessageTypes from "/snippets/webview-message-types.mdx"; This documentation covers integrating WalletConnect Pay through ReownWalletKit. This approach provides a unified API where Pay is automatically initialized alongside WalletKit, simplifying the integration for wallet developers. @@ -91,6 +94,7 @@ The payment flow consists of five main steps: sequenceDiagram participant User participant Wallet + participant WebView participant WalletKit as WalletKit.Pay participant Backend as WalletConnect Pay @@ -113,8 +117,10 @@ sequenceDiagram User->>Wallet: Approve & sign alt Data collection required - Wallet->>User: Request additional info - User->>Wallet: Provide data + Wallet->>WebView: Load collectDataAction.url in WebView + WebView->>User: Display data collection form + User->>WebView: Fill form & accept T&C + WebView-->>Wallet: IC_COMPLETE message end Wallet->>WalletKit: confirmPayment(request) @@ -182,21 +188,30 @@ for (final action in actions) { Some payments may require additional user data: -```dart -List? collectedData; + -if (response.collectData != null) { - collectedData = []; - for (final field in response.collectData!.fields) { - // Collect data from user (e.g., full name, date of birth) - collectedData.add(CollectDataFieldResult( - id: field.id, - value: userInput, - )); - } +```dart +if (response.collectData?.url != null) { + // Use the "required" list from response.collectData.schema to determine which fields to prefill + final prefillData = { + 'fullName': 'John Doe', + 'dateOfBirth': '1990-01-15', + 'pobAddress': '123 Main St, New York, NY 10001', + }; + final prefillJson = jsonEncode(prefillData); + final prefillBase64 = base64Url.encode(utf8.encode(prefillJson)); + final uri = Uri.parse(response.collectData!.url); + final webViewUrl = uri.replace( + queryParameters: {...uri.queryParameters, 'prefill': prefillBase64}, + ).toString(); + + // Show WebView and wait for IC_COMPLETE message + showDataCollectionWebView(webViewUrl); } ``` + + @@ -225,6 +240,91 @@ print('Is Final: ${confirmResponse.isFinal}'); +## WebView Implementation + +When `collectData.url` is present, display the URL in a WebView using `webview_flutter` (v4.10.0+). Add dependencies: + +```yaml +dependencies: + webview_flutter: ^4.10.0 + url_launcher: ^6.1.0 +``` + +```dart +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class PayDataCollectionWebView extends StatefulWidget { + final String url; + final VoidCallback onComplete; + final ValueChanged onError; + + const PayDataCollectionWebView({ + super.key, + required this.url, + required this.onComplete, + required this.onError, + }); + + @override + State createState() => + _PayDataCollectionWebViewState(); +} + +class _PayDataCollectionWebViewState extends State { + late final WebViewController _controller; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => setState(() => _isLoading = false), + onNavigationRequest: (request) { + if (!request.url.contains('pay.walletconnect.com')) { + launchUrl(Uri.parse(request.url), + mode: LaunchMode.externalApplication); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + )) + ..addJavaScriptChannel( + 'ReactNativeWebView', + onMessageReceived: (message) { + try { + final data = jsonDecode(message.message) as Map; + switch (data['type']) { + case 'IC_COMPLETE': + widget.onComplete(); + break; + case 'IC_ERROR': + widget.onError(data['error'] ?? 'Unknown error'); + break; + } + } catch (_) {} + }, + ) + ..loadRequest(Uri.parse(widget.url)); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + WebViewWidget(controller: _controller), + if (_isLoading) + const Center(child: CircularProgressIndicator()), + ], + ); + } +} +``` + ## Complete Example Here's a complete example of processing a payment: @@ -254,13 +354,9 @@ class PaymentService { throw Exception('No payment options available'); } - // Step 2: Collect additional data if required (KYB/KYC) - final collectedData = []; - if (optionsResponse.collectData != null) { - for (final field in optionsResponse.collectData!.fields) { - // Collect data from user (e.g., full name, date of birth) - // Example: collectedData.add(CollectDataFieldResult(id: field.id, value: userInput)); - } + // Step 2: Collect data via WebView if required + if (optionsResponse.collectData?.url != null) { + await showDataCollectionWebView(optionsResponse.collectData!.url); } // Step 3: Select payment option (or let user choose) @@ -286,13 +382,12 @@ class PaymentService { // Example: signatures.add(await signTransaction(action.walletRpc)); } - // Step 6: Confirm payment with polling + // Step 6: Confirm payment ConfirmPaymentResponse confirmResponse = await walletKit.confirmPayment( request: ConfirmPaymentRequest( paymentId: paymentId, optionId: optionId, signatures: signatures, - collectedData: collectedData.isNotEmpty ? collectedData : null, maxPollMs: 60000, // Maximum polling time in milliseconds ), ); @@ -305,7 +400,6 @@ class PaymentService { paymentId: paymentId, optionId: optionId, signatures: signatures, - collectedData: collectedData.isNotEmpty ? collectedData : null, maxPollMs: 60000, ), ); @@ -382,9 +476,19 @@ PaymentOptionsResponse({ PaymentInfo? info, required List options, CollectDataAction? collectData, + PaymentResultInfo? resultInfo, // Transaction result details (present when payment already completed) }) ``` +#### PaymentResultInfo + +```dart +class PaymentResultInfo { + final String txId; // Transaction ID + final PayAmount optionAmount; // Token amount details +} +``` + #### PaymentInfo ```dart @@ -416,7 +520,6 @@ ConfirmPaymentRequest({ required String paymentId, required String optionId, required List signatures, - List? collectedData, int? maxPollMs, }) ``` @@ -443,6 +546,15 @@ enum PaymentStatus { } ``` +#### CollectDataAction + +```dart +class CollectDataAction { + final String url; // WebView URL for data collection + final String? schema; // JSON schema describing required fields +} +``` + ## Error Handling The SDK throws specific exception types for different error scenarios. All errors extend the abstract `PayError` class, which itself extends `PlatformException`: @@ -507,6 +619,8 @@ try { 9. **User Data**: Only collect data when `collectData` is present in the response and you don't already have the required user data. If you already have the required data, you can submit this without collecting from the user. You must make sure the user accepts WalletConnect Terms and Conditions and Privacy Policy before submitting user information to WalletConnect. +10. **WebView Data Collection**: When `collectData.url` is present, display the URL in a WebView using `webview_flutter` rather than building native forms. The WebView handles form rendering, validation, and T&C acceptance. + ## Examples For a complete example implementation with UI components showing the full payment flow, see the [reown_walletkit example](https://github.com/reown-com/reown_flutter/tree/master/packages/reown_walletkit/example/lib/walletconnect_pay). diff --git a/payments/wallets/walletkit/kotlin.mdx b/payments/wallets/walletkit/kotlin.mdx index 175b0ca..9709ebe 100644 --- a/payments/wallets/walletkit/kotlin.mdx +++ b/payments/wallets/walletkit/kotlin.mdx @@ -4,6 +4,9 @@ description: "Integrate WalletConnect Pay through WalletKit for a unified paymen sidebarTitle: "Kotlin" --- import AppIdSnippet from "/snippets/app-id.mdx"; +import WebViewOverview from "/snippets/webview-data-collection-overview.mdx"; +import WebViewBestPractices from "/snippets/webview-data-collection-best-practices.mdx"; +import WebViewMessageTypes from "/snippets/webview-message-types.mdx"; This documentation covers integrating WalletConnect Pay through WalletKit. This approach provides a unified API where Pay is automatically initialized alongside WalletKit, simplifying the integration for wallet developers. @@ -115,6 +118,7 @@ sequenceDiagram participant Wallet participant WalletKit as WalletKit.Pay participant Backend as WalletConnect Pay + participant WebView User->>Wallet: Scan QR / Open payment link Wallet->>WalletKit: isPaymentLink(uri) @@ -135,8 +139,10 @@ sequenceDiagram User->>Wallet: Approve & sign alt Data collection required - Wallet->>User: Request additional info - User->>Wallet: Provide data + Wallet->>WebView: Load collectDataAction.url in WebView + WebView->>User: Display data collection form + User->>WebView: Fill form & accept T&C + WebView-->>Wallet: IC_COMPLETE message end Wallet->>WalletKit: confirmPayment(params) @@ -280,21 +286,35 @@ Signatures must be in the same order as the actions array. Some payments require additional user information: + + ```kotlin response.collectDataAction?.let { collectAction -> - val collectedData = collectAction.fields.map { field -> - val value = when (field.fieldType) { - Wallet.Model.CollectDataFieldType.TEXT -> getUserTextInput(field.name) - Wallet.Model.CollectDataFieldType.DATE -> getUserDateInput(field.name) // YYYY-MM-DD - } - Wallet.Model.CollectDataFieldResult( - id = field.id, - value = value + val url = collectAction.url + if (url != null) { + // Build prefill URL with known user data + // Use the "required" list from collectAction.schema to determine which fields to prefill + val prefillJson = JSONObject().apply { + put("fullName", "John Doe") + put("dateOfBirth", "1990-01-15") + put("pobAddress", "123 Main St, New York, NY 10001") + }.toString() + val prefillBase64 = Base64.encodeToString( + prefillJson.toByteArray(), + Base64.NO_WRAP or Base64.URL_SAFE ) + val webViewUrl = Uri.parse(url).buildUpon() + .appendQueryParameter("prefill", prefillBase64) + .build().toString() + + // Show WebView and wait for IC_COMPLETE message + showWebView(webViewUrl) } } ``` + + @@ -338,6 +358,70 @@ confirmResult.onSuccess { response -> +## WebView Implementation + +When `collectDataAction.url` is present, display the URL in a WebView. The WebView handles form rendering, validation, and T&C acceptance. + +```kotlin +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient +import android.webkit.WebResourceRequest +import android.content.Intent +import android.net.Uri +import android.util.Base64 +import androidx.compose.runtime.Composable +import androidx.compose.ui.viewinterop.AndroidView +import org.json.JSONObject + +@Composable +fun PayDataCollectionWebView( + url: String, + onComplete: () -> Unit, + onError: (String) -> Unit +) { + AndroidView(factory = { context -> + WebView(context).apply { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.allowFileAccess = false + + addJavascriptInterface( + object { + @JavascriptInterface + fun onDataCollectionComplete(json: String) { + val message = JSONObject(json) + when (message.optString("type")) { + "IC_COMPLETE" -> onComplete() + "IC_ERROR" -> onError( + message.optString("error", "Unknown error") + ) + } + } + }, + "AndroidWallet" + ) + + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val requestUrl = request?.url?.toString() ?: return false + if (!requestUrl.contains("pay.walletconnect.com")) { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(requestUrl))) + return true + } + return false + } + } + + loadUrl(url) + } + }) +} +``` + ## Complete Example Here's a complete implementation example: @@ -388,9 +472,10 @@ class PaymentViewModel : ViewModel() { // Step 3: Sign actions val signatures = signActions(actions) - // Step 4: Collect data if required - val collectedData = response.collectDataAction?.let { - collectUserData(it.fields) + // Step 4: Collect data via WebView if required + response.collectDataAction?.url?.let { webViewUrl -> + showDataCollectionWebView(webViewUrl) + return@launch } // Step 5: Confirm payment @@ -398,8 +483,7 @@ class PaymentViewModel : ViewModel() { Wallet.Params.ConfirmPayment( paymentId = paymentId, optionId = selectedOption.id, - signatures = signatures, - collectedData = collectedData + signatures = signatures ) ) @@ -473,8 +557,7 @@ data class RequiredPaymentActions( data class ConfirmPayment( val paymentId: String, val optionId: String, - val signatures: List, - val collectedData: List? = null + val signatures: List ) ``` @@ -487,7 +570,13 @@ data class PaymentOptionsResponse( val paymentId: String, val info: PaymentInfo?, val options: List, - val collectDataAction: CollectDataAction? + val collectDataAction: CollectDataAction?, + val resultInfo: PaymentResultInfo? // Transaction result details (present when payment already completed) +) + +data class PaymentResultInfo( + val txId: String, // Transaction ID + val optionAmount: PaymentAmount // Token amount details ) ``` @@ -547,6 +636,25 @@ data class WalletRpcAction( ) ``` +### Wallet.Model.CollectDataAction + +```kotlin +data class CollectDataAction( + val url: String, // WebView URL for data collection + val schema: String? // JSON schema describing required fields +) +``` + +### Wallet.Model.ConfirmPaymentResponse + +```kotlin +data class ConfirmPaymentResponse( + val status: PaymentStatus, + val isFinal: Boolean, + val pollInMs: Long? +) +``` + ### Wallet.Model.PaymentStatus | Status | Description | @@ -576,3 +684,5 @@ data class WalletRpcAction( 8. **Expiration**: Check `paymentInfo.expiresAt` and warn users if time is running low 9. **User Data**: Only collect data when `collectData` is present in the response and you don't already have the required user data. If you already have the required data, you can submit this without collecting from the user. You must make sure the user accepts WalletConnect Terms and Conditions and Privacy Policy before submitting user information to WalletConnect. + +10. **WebView Data Collection**: When `collectDataAction.url` is present, display the URL in a WebView rather than building native forms. The WebView handles form rendering, validation, and T&C acceptance. diff --git a/payments/wallets/walletkit/swift.mdx b/payments/wallets/walletkit/swift.mdx index 690660d..be2f467 100644 --- a/payments/wallets/walletkit/swift.mdx +++ b/payments/wallets/walletkit/swift.mdx @@ -5,6 +5,9 @@ sidebarTitle: "Swift" --- import AppIdSnippet from "/snippets/app-id.mdx"; +import WebViewOverview from "/snippets/webview-data-collection-overview.mdx"; +import WebViewBestPractices from "/snippets/webview-data-collection-best-practices.mdx"; +import WebViewMessageTypes from "/snippets/webview-message-types.mdx"; This documentation covers integrating WalletConnect Pay through WalletKit. This approach provides a unified API where Pay is automatically configured when you configure WalletKit, simplifying the integration for wallet developers. @@ -103,6 +106,7 @@ sequenceDiagram participant Wallet participant WalletKit as WalletKit.Pay participant Backend as WalletConnect Pay + participant WebView User->>Wallet: Scan QR / Open payment link Wallet->>WalletKit: isPaymentLink(uri) @@ -123,8 +127,10 @@ sequenceDiagram User->>Wallet: Approve & sign alt Data collection required - Wallet->>User: Request additional info - User->>Wallet: Provide data + Wallet->>WebView: Load collectDataAction.url in WebView + WebView->>User: Display data collection form + User->>WebView: Fill form & accept T&C + WebView-->>Wallet: IC_COMPLETE message end Wallet->>WalletKit: confirmPayment(params) @@ -230,24 +236,31 @@ Signatures must be in the same order as the actions array. If `options.collectData` is not nil, you must collect user information before confirming: -```swift -var collectedData: [CollectDataFieldResult]? = nil -if let collectData = options.collectData { - collectedData = try await collectUserData(fields: collectData.fields) -} + -private func collectUserData( - fields: [CollectDataField] -) async throws -> [CollectDataFieldResult] { - return fields.map { field in - CollectDataFieldResult( - id: field.id, - value: getUserInput(for: field) - ) - } +```swift +if let collectData = options.collectData, let url = collectData.url { + // Use the "required" list from collectData.schema to determine which fields to prefill + let prefillData: [String: String] = [ + "fullName": "John Doe", + "dateOfBirth": "1990-01-15", + "pobAddress": "123 Main St, New York, NY 10001" + ] + let jsonData = try JSONSerialization.data(withJSONObject: prefillData) + let prefillBase64 = jsonData.base64EncodedString() + var components = URLComponents(string: url)! + var queryItems = components.queryItems ?? [] + queryItems.append(URLQueryItem(name: "prefill", value: prefillBase64)) + components.queryItems = queryItems + let webViewUrl = components.string! + + // Show WebView and wait for IC_COMPLETE message + showWebView(url: webViewUrl) } ``` + + @@ -281,6 +294,89 @@ case .requiresAction: +## WebView Implementation + +When `collectData.url` is present, display the URL in a `WKWebView`. The WebView handles form rendering, validation, and T&C acceptance. + +```swift +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) + } + } +} +``` + ## Complete Example Here's a complete implementation using WalletKit: @@ -332,18 +428,16 @@ class PaymentManager { signatures.append(signature) } - // 5. Collect user data if required - var collectedData: [CollectDataFieldResult]? = nil - if let collectData = optionsResponse.collectData { - collectedData = try await collectUserData(fields: collectData.fields) + // 5. Collect data via WebView if required + if let collectData = optionsResponse.collectData, let url = collectData.url { + try await showDataCollectionWebView(url: url) } - + // 6. Confirm payment let result = try await WalletKit.instance.Pay.confirmPayment( paymentId: optionsResponse.paymentId, optionId: selectedOption.id, - signatures: signatures, - collectedData: collectedData + signatures: signatures ) guard result.status == .succeeded else { @@ -371,17 +465,6 @@ class PaymentManager { address: walletAddress ) } - - private func collectUserData( - fields: [CollectDataField] - ) async throws -> [CollectDataFieldResult] { - return fields.map { field in - CollectDataFieldResult( - id: field.id, - value: getUserInput(for: field) - ) - } - } } ``` @@ -433,7 +516,7 @@ When using WalletKit, Pay methods are accessed via `WalletKit.instance.Pay.*`. | `isPaymentLink(_:)` | Check if a string is a payment link | | `getPaymentOptions(paymentLink:accounts:includePaymentInfo:)` | Fetch available payment options | | `getRequiredPaymentActions(paymentId:optionId:)` | Get signing actions for a payment option | -| `confirmPayment(paymentId:optionId:signatures:collectedData:maxPollMs:)` | Confirm and execute the payment | +| `confirmPayment(paymentId:optionId:signatures:maxPollMs:)` | Confirm and execute the payment | ### Data Types @@ -445,6 +528,12 @@ struct PaymentOptionsResponse { let info: PaymentInfo? // Merchant and amount details let options: [PaymentOption] // Available payment methods let collectData: CollectDataAction? // Required user data fields (travel rule) + let resultInfo: PaymentResultInfo? // Transaction result details (present when payment already completed) +} + +struct PaymentResultInfo { + let txId: String // Transaction ID + let optionAmount: PayAmount // Token amount details } ``` @@ -503,6 +592,25 @@ struct WalletRpcAction { } ``` +#### CollectDataAction + +```swift +struct CollectDataAction { + let url: String // WebView URL for data collection + let schema: String? // JSON schema describing required fields +} +``` + +#### ConfirmPaymentResultResponse + +```swift +struct ConfirmPaymentResultResponse { + let status: PaymentStatus // Final payment status + let isFinal: Bool // Whether status is final + let pollInMs: Int64? // Suggested poll interval +} +``` + #### PaymentStatus ```swift @@ -570,3 +678,5 @@ The SDK throws specific error types for different failure scenarios: 8. **Expiration**: Check `paymentInfo.expiresAt` and warn users if time is running low 9. **User Data**: Only collect data when `collectData` is present in the response and you don't already have the required user data. If you already have the required data, you can submit this without collecting from the user. You must make sure the user accepts WalletConnect Terms and Conditions and Privacy Policy before submitting user information to WalletConnect. + +10. **WebView Data Collection**: When `collectData.url` is present, display the URL in a WKWebView rather than building native forms. The WebView handles form rendering, validation, and T&C acceptance. diff --git a/payments/wallets/walletkit/web.mdx b/payments/wallets/walletkit/web.mdx index f65494f..437a361 100644 --- a/payments/wallets/walletkit/web.mdx +++ b/payments/wallets/walletkit/web.mdx @@ -5,6 +5,9 @@ sidebarTitle: "React Native" --- import AppIdSnippet from "/snippets/app-id.mdx"; +import WebViewOverview from "/snippets/webview-data-collection-overview.mdx"; +import WebViewBestPractices from "/snippets/webview-data-collection-best-practices.mdx"; +import WebViewMessageTypes from "/snippets/webview-message-types.mdx"; WalletConnect Pay is currently only available on React Native and requires `@walletconnect/react-native-compat` to be installed. Web/Node.js versions will be available soon. @@ -98,6 +101,7 @@ sequenceDiagram participant Wallet participant WalletKit as WalletKit.Pay participant Backend as WalletConnect Pay + participant WebView User->>Wallet: Scan QR / Open payment link Wallet->>WalletKit: isPaymentLink(uri) @@ -118,8 +122,10 @@ sequenceDiagram User->>Wallet: Approve & sign alt Data collection required - Wallet->>User: Request additional info - User->>Wallet: Provide data + Wallet->>WebView: Load collectDataAction.url in WebView + WebView->>User: Display data collection form + User->>WebView: Fill form & accept T&C + WebView-->>Wallet: IC_COMPLETE message end Wallet->>WalletKit: pay.confirmPayment(params) @@ -211,18 +217,27 @@ Signatures must be in the same order as the actions array. Some payments may require additional user data: -```javascript -let collectedData; + -if (options.collectData) { - // Show UI to collect required fields - collectedData = options.collectData.fields.map((field) => ({ - id: field.id, - value: getUserInput(field.name, field.fieldType), - })); +```javascript +if (options.collectData?.url) { + // Use the "required" list from options.collectData.schema to determine which fields to prefill + const prefillData = { + fullName: "John Doe", + dateOfBirth: "1990-01-15", + pobAddress: "123 Main St, New York, NY 10001", + }; + const prefillBase64 = btoa(JSON.stringify(prefillData)); + const separator = options.collectData.url.includes("?") ? "&" : "?"; + const webViewUrl = `${options.collectData.url}${separator}prefill=${prefillBase64}`; + + // Show WebView and wait for IC_COMPLETE message + showDataCollectionWebView(webViewUrl); } ``` + + @@ -254,6 +269,72 @@ if (result.status === "succeeded") { +## WebView Implementation + +When `collectData.url` is present, display the URL in a WebView using `react-native-webview`. Install the dependency: + +```bash +npm install react-native-webview@13.16.0 +``` + +```jsx +import React, { useCallback } from "react"; +import { WebView } from "react-native-webview"; +import { Linking, View, ActivityIndicator } from "react-native"; + +function PayDataCollectionWebView({ url, onComplete, onError }) { + const handleMessage = useCallback( + (event) => { + try { + const data = JSON.parse(event.nativeEvent.data); + switch (data.type) { + case "IC_COMPLETE": + onComplete(); + break; + case "IC_ERROR": + onError(data.error || "Unknown error"); + break; + } + } catch { + // Ignore non-JSON messages + } + }, + [onComplete, onError] + ); + + const handleNavigationRequest = useCallback((request) => { + if (!request.url.includes("pay.walletconnect.com")) { + Linking.openURL(request.url); + return false; + } + return true; + }, []); + + return ( + ( + + + + )} + /> + ); +} + +function buildPrefillUrl(baseUrl, prefillData) { + if (Object.keys(prefillData).length === 0) return baseUrl; + const base64 = btoa(JSON.stringify(prefillData)); + const separator = baseUrl.includes("?") ? "&" : "?"; + return `${baseUrl}${separator}prefill=${base64}`; +} +``` + ## Complete Example Here's a complete implementation example: @@ -322,10 +403,9 @@ class PaymentManager { actions.map((action) => this.signAction(action, walletAddress)) ); - // Step 5: Collect user data if required - let collectedData; - if (options.collectData) { - collectedData = await this.collectUserData(options.collectData.fields); + // Step 5: Collect data via WebView if required + if (options.collectData?.url) { + await this.showDataCollectionWebView(options.collectData.url); } // Step 6: Confirm payment @@ -333,7 +413,6 @@ class PaymentManager { paymentId: options.paymentId, optionId: selectedOption.id, signatures, - collectedData, }); return result; @@ -346,18 +425,10 @@ class PaymentManager { async signAction(action, walletAddress) { const { chainId, method, params } = action.walletRpc; const parsedParams = JSON.parse(params); - + // Use your wallet's signing implementation return await wallet.signTypedData(chainId, parsedParams); } - - async collectUserData(fields) { - // Implement your UI to collect user data - return fields.map((field) => ({ - id: field.id, - value: getUserInput(field.name, field.fieldType), - })); - } } ``` @@ -409,7 +480,6 @@ interface ConfirmPaymentParams { paymentId: string; // Payment ID optionId: string; // Selected option ID signatures: string[]; // Signatures from wallet RPC calls - collectedData?: CollectDataFieldResult[]; // Collected user data } ``` @@ -423,6 +493,12 @@ interface PaymentOptionsResponse { info?: PaymentInfo; // Payment information options: PaymentOption[]; // Available payment options collectData?: CollectDataAction; // Data collection requirements + resultInfo?: PaymentResultInfo; // Transaction result details (present when payment already completed) +} + +interface PaymentResultInfo { + txId: string; // Transaction ID + optionAmount: PayAmount; // Token amount details } ``` @@ -460,11 +536,11 @@ interface ConfirmPaymentResponse { pollInMs?: number; // Suggested poll interval } -type PaymentStatus = - | "requires_action" - | "processing" - | "succeeded" - | "failed" +type PaymentStatus = + | "requires_action" + | "processing" + | "succeeded" + | "failed" | "expired"; ``` @@ -507,19 +583,10 @@ interface AmountDisplay { ```typescript interface CollectDataAction { - fields: CollectDataField[]; -} - -interface CollectDataField { - id: string; // Field identifier - name: string; // Display name - required: boolean; // Whether field is required - fieldType: "text" | "date"; // Type of input needed -} - -interface CollectDataFieldResult { - id: string; // Field identifier - value: string; // User-provided value + /** WebView URL for data collection */ + url: string; + /** JSON schema describing required fields */ + schema?: string; } ``` @@ -563,3 +630,5 @@ try { 8. **Expiration**: Check `paymentInfo.expiresAt` and warn users if time is running low 9. **User Data**: Only collect data when `collectData` is present in the response and you don't already have the required user data. If you already have the required data, you can submit this without collecting from the user. You must make sure the user accepts WalletConnect Terms and Conditions and Privacy Policy before submitting user information to WalletConnect. + +10. **WebView Data Collection**: When `collectData.url` is present, display the URL in a WebView using `react-native-webview` rather than building native forms. The WebView handles form rendering, validation, and T&C acceptance. diff --git a/snippets/webview-data-collection-best-practices.mdx b/snippets/webview-data-collection-best-practices.mdx new file mode 100644 index 0000000..6841e5f --- /dev/null +++ b/snippets/webview-data-collection-best-practices.mdx @@ -0,0 +1,9 @@ +### WebView Data Collection Best Practices + +- **Display prominently**: Show the WebView full-screen or as a prominent modal so users can interact with the form easily +- **Loading indicator**: Show a loading indicator while the WebView page loads +- **Handle errors**: Listen for `IC_ERROR` messages and display a user-facing error message with an option to retry +- **External links**: Open Terms & Conditions and Privacy Policy links in the system browser rather than navigating within the WebView +- **Domain restriction**: Only allow navigation to WalletConnect pay domains and HTTPS URLs +- **Back navigation**: Handle back/dismiss gracefully — confirm cancellation with the user before closing the WebView mid-flow +- **Keyboard behavior**: Test that the soft keyboard appears and behaves correctly when users tap on form inputs within the WebView diff --git a/snippets/webview-data-collection-overview.mdx b/snippets/webview-data-collection-overview.mdx new file mode 100644 index 0000000..d07a81f --- /dev/null +++ b/snippets/webview-data-collection-overview.mdx @@ -0,0 +1,21 @@ +## WebView-Based Data Collection + +When a payment requires user information (e.g., for Travel Rule compliance), the SDK returns a `collectDataAction` with a `url` field pointing to a WalletConnect-hosted form page. + +Instead of building native forms, wallets display this URL in a WebView. The hosted form handles rendering, validation, and Terms & Conditions acceptance. When the user completes the form, the WebView communicates back to the wallet via JavaScript bridge messages. + +### How It Works + +1. Check if `collectDataAction.url` is present in the payment options response +2. Open the URL in a WebView within your wallet +3. Optionally append a `prefill=` query parameter with known user data (e.g., name, date of birth, address). Use proper URL building to handle existing query parameters. +4. Listen for JS bridge messages: `IC_COMPLETE` (success) or `IC_ERROR` (failure) +5. On `IC_COMPLETE`, proceed to `confirmPayment()` **without** passing `collectedData` — the WebView submits data directly to the backend + + +The `collectDataAction` also includes a `schema` field — a JSON schema string describing the required fields. The `required` list in this schema tells you which fields the form expects. Wallets can use these field names as keys when building the prefill JSON object. For example, if the schema's `required` array contains `["fullName", "dateOfBirth", "pobAddress"]`, you can prefill with `{"fullName": "...", "dateOfBirth": "...", "pobAddress": "..."}`. + + + +When using the WebView approach, do **not** pass `collectedData` to `confirmPayment()`. The WebView handles data submission directly. + diff --git a/snippets/webview-message-types.mdx b/snippets/webview-message-types.mdx new file mode 100644 index 0000000..d143d55 --- /dev/null +++ b/snippets/webview-message-types.mdx @@ -0,0 +1,17 @@ +### WebView Message Types + +The WebView communicates with your wallet through JavaScript bridge messages. The message payload is a JSON string with the following structure: + +| Message Type | Payload | Description | +|-------------|---------|-------------| +| `IC_COMPLETE` | `{ "type": "IC_COMPLETE", "success": true }` | User completed the form successfully. Proceed to payment confirmation. | +| `IC_ERROR` | `{ "type": "IC_ERROR", "error": "..." }` | An error occurred. Display the error message and allow the user to retry. | + +#### Platform-Specific Bridge Names + +| Platform | Bridge Name | Handler | +|----------|------------|---------| +| Kotlin (Android) | `AndroidWallet` | `@JavascriptInterface onDataCollectionComplete(json: String)` | +| Swift (iOS) | `payDataCollectionComplete` | `WKScriptMessageHandler.didReceive(message:)` | +| Flutter | `ReactNativeWebView` (injected via JS bridge) | `JavaScriptChannel.onMessageReceived` | +| React Native | `ReactNativeWebView` (native) | `WebView.onMessage` prop |