Skip to content

Comments

feat(pay): add WebView-based Information Capture for Pay SDK#254

Merged
jakubuid merged 11 commits intodevelopfrom
feat/ic_webview
Feb 11, 2026
Merged

feat(pay): add WebView-based Information Capture for Pay SDK#254
jakubuid merged 11 commits intodevelopfrom
feat/ic_webview

Conversation

@jakubuid
Copy link
Collaborator

@jakubuid jakubuid commented Feb 2, 2026

Summary

Add WebView-based Information Capture (IC) for the payment flow. When collectDataAction.url is present in the API response, show a WebView instead of field-by-field data collection.

Based on Kotlin PR: reown-com/reown-kotlin#276

Key Changes

  • WebView Information Capture: When collectData.url is present, load WebView instead of native form fields
  • JavaScript Bridge: Register payDataCollectionComplete message handler for WebView → Wallet communication
  • Payment Result Info: Display transaction ID and token amount on success screen
  • Flow Updates: Added webviewDataCollection step to PayFlowStep enum

Files Changed

File Changes
Package.swift Updated Yttrium to 0.10.28
PayPresenter.swift Added webview flow step, IC handlers, result info storage
PayDataCollectionWebView.swift NEW - WKWebView wrapper with JS message handler
PayContainerView.swift Added case for webview step
PaySuccessView.swift Added result info display (txId + token amount)

JavaScript Bridge Protocol

The IC page communicates via:

window.webkit.messageHandlers.payDataCollectionComplete.postMessage({
    type: 'IC_COMPLETE',
    success: true
});

Flow Diagram

collectData.url exists? → Show WebView → IC_COMPLETE → Payment Options → Success
collectData.fields only? → Show native forms → Payment Options → Success
No collectData? → Skip to Payment Options → Success

Test plan

  • Scan payment QR that requires IC with collectData.url
  • Verify WebView loads and displays form
  • Fill form and submit
  • Verify IC_COMPLETE triggers transition to payment options
  • Complete payment and verify success screen

🤖 Generated with Claude Code

jakubuid and others added 3 commits February 2, 2026 12:02
- Add webviewDataCollection flow step when collectData.url is present
- Create PayDataCollectionWebView component with JS bridge (iOSWallet)
- Handle IC_COMPLETE and IC_ERROR messages from WebView
- Display payment result info (txId, token amount) on success screen
- Update Yttrium dependency to 0.10.28

Based on Kotlin PR #276

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change handler from 'iOSWallet' to 'payDataCollectionComplete' to match
the IC page's expected interface for iOS WKWebView communication.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 2, 2026

Claude finished @jakubuid's task in 1m 43s —— View job


Code Review Complete

Reviewing PR #254: WebView-based Information Capture for Pay SDK


Issue 1: Missing URL validation before loading in WebView

ID: paydatacoll-url-validation-a7c3
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift:26
Severity: HIGH
Category: security

The WebView loads URLs directly without validation. A malicious or compromised API response could provide URLs to phishing sites, malicious content, or execute XSS attacks.

Recommendation: Validate URL domain against allowlist before loading:

func makeUIView(context: Context) -> WKWebView {
    let config = WKWebViewConfiguration()
    
    // Validate URL domain
    guard isAllowedDomain(url) else {
        context.coordinator.onError("Invalid URL domain")
        return WKWebView(frame: .zero, configuration: config)
    }
    
    // Register message handler matching the IC page's expected name
    config.userContentController.add(context.coordinator, name: "payDataCollectionComplete")
    // ... rest of setup
}

private func isAllowedDomain(_ url: URL) -> Bool {
    guard let host = url.host else { return false }
    let allowedDomains = ["pay.walletconnect.com", "pay.reown.com"]
    return allowedDomains.contains { host == $0 || host.hasSuffix(".\($0)") }
}

Issue 2: Missing JavaScript injection restrictions

ID: paydatacoll-js-injection-b8f1
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift:14
Severity: HIGH
Category: security

WKWebViewConfiguration lacks restrictions on JavaScript execution. If the loaded page is compromised, it could execute arbitrary JavaScript and potentially extract data from the message handler.

Recommendation: Configure security preferences and disable unnecessary features:

let config = WKWebViewConfiguration()

// Restrict JavaScript capabilities
let preferences = WKWebpagePreferences()
preferences.allowsContentJavaScript = true // Required for message handler
config.defaultWebpagePreferences = preferences

// Disable data detection
config.dataDetectorTypes = []

// Prevent app links
config.preferences.javaScriptCanOpenWindowsAutomatically = false

Issue 3: Missing TLS/SSL certificate validation

ID: paydatacoll-ssl-validation-c2d4
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift:20
Severity: HIGH
Category: security

No WKNavigationDelegate method implements SSL certificate validation. An attacker performing MITM attack could intercept payment data collection.

Recommendation: Implement SSL pinning or strict validation:

func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, 
             completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
          let serverTrust = challenge.protectionSpace.serverTrust else {
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }
    
    // Verify host matches expected domains
    let host = challenge.protectionSpace.host
    let allowedHosts = ["pay.walletconnect.com", "pay.reown.com"]
    
    if allowedHosts.contains(host) {
        completionHandler(.useCredential, URLCredential(trust: serverTrust))
    } else {
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
}

Issue 4: Missing navigation policy restrictions

ID: paydatacoll-nav-policy-e5f8
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift:32
Severity: MEDIUM
Category: security

Coordinator doesn't implement decidePolicyFor navigationAction. WebView could navigate to external/malicious URLs via links, redirects, or JavaScript without restriction.

Recommendation: Restrict navigation to allowed domains:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    guard let url = navigationAction.request.url,
          let host = url.host else {
        decisionHandler(.cancel)
        return
    }
    
    let allowedDomains = ["pay.walletconnect.com", "pay.reown.com"]
    let isAllowed = allowedDomains.contains { host == $0 || host.hasSuffix(".\($0)") }
    
    decisionHandler(isAllowed ? .allow : .cancel)
}

Issue 5: Message handler not removed on cleanup

ID: paydatacoll-handler-leak-f1a9
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift:17
Severity: MEDIUM
Category: code_quality

WKScriptMessageHandler is added but never removed. This can cause memory leaks as Coordinator is retained by userContentController.

Recommendation: Implement cleanup method:

func makeUIView(context: Context) -> WKWebView {
    let config = WKWebViewConfiguration()
    config.userContentController.add(context.coordinator, name: "payDataCollectionComplete")
    let webView = WKWebView(frame: .zero, configuration: config)
    context.coordinator.userContentController = config.userContentController
    // ... rest
}

class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate, WKUIDelegate {
    weak var userContentController: WKUserContentController?
    
    deinit {
        userContentController?.removeScriptMessageHandler(forName: "payDataCollectionComplete")
    }
}

Issue 6: No message origin validation

ID: paydatacoll-origin-check-d3b7
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift:42
Severity: HIGH
Category: security

JavaScript messages are accepted without verifying frame origin. Malicious iframes or injected scripts could send spoofed completion messages.

Recommendation: Validate message frame origin:

func userContentController(_ userContentController: WKUserContentController,
                           didReceive message: WKScriptMessage) {
    // Validate frame origin
    guard let frameHost = message.frameInfo.securityOrigin.host,
          isAllowedDomain(frameHost) else {
        print("💳 [PayWebView] Rejected message from unauthorized origin")
        onError("Message from unauthorized source")
        return
    }
    
    print("💳 [PayWebView] Received message: \(message.body)")
    // ... rest of validation
}

private func isAllowedDomain(_ host: String) -> Bool {
    let allowedDomains = ["pay.walletconnect.com", "pay.reown.com"]
    return allowedDomains.contains { host == $0 || host.hasSuffix(".\($0)") }
}

Issue 7: Sensitive payment data in console logs

ID: paypresenter-sensitive-logs-a8c2
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PayPresenter.swift:85
Severity: MEDIUM
Category: security

Multiple print statements log potentially sensitive payment information including collectData fields, payment options, and user input.

Recommendation: Remove or guard debug logs:

#if DEBUG
print("💳 [Pay] getPaymentOptions response: \(response)")
#endif

Issue 8: Missing validation for IC completion success flag

ID: paydatacoll-success-validation-b2c8
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift:59
Severity: MEDIUM
Category: code_quality

When IC_COMPLETE message has success: false, no error callback is triggered, causing silent failure. User would be stuck without feedback.

Recommendation: Handle failure case explicitly:

case "IC_COMPLETE":
    let success = body["success"] as? Bool ?? false
    let message = body["message"] as? String
    print("💳 [PayWebView] IC_COMPLETE received, success: \(success)")
    if success {
        DispatchQueue.main.async { [weak self] in
            self?.onComplete()
        }
    } else {
        DispatchQueue.main.async { [weak self] in
            self?.onError(message ?? "Data collection was not completed")
        }
    }

Issue 9: Potential division by zero in token amount formatting

ID: paysuccessvi-divide-zero-c9d4
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PaySuccessView.swift:132
Severity: LOW
Category: code_quality

If amount.display.decimals is invalid (negative or extremely large), pow(10.0, Double(decimals)) could cause division by zero or overflow.

Recommendation: Add bounds check:

private func formatTokenAmount(_ amount: PayAmount) -> String {
    let value = Double(amount.value) ?? 0
    let decimals = max(0, min(Int(amount.display.decimals), 18)) // Clamp to reasonable range
    let displayValue = value / pow(10.0, Double(decimals))
    // ... rest
}

Issue 10: No timeout for WebView page load

ID: paydatacoll-load-timeout-e1f2
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift:26
Severity: MEDIUM
Category: code_quality

WebView load has no timeout. If IC page hangs or loads slowly, user is stuck indefinitely with no fallback.

Recommendation: Implement timeout mechanism:

func makeUIView(context: Context) -> WKWebView {
    let config = WKWebViewConfiguration()
    // ... setup
    
    let webView = WKWebView(frame: .zero, configuration: config)
    // ... setup
    
    // Set timeout
    DispatchQueue.main.asyncAfter(deadline: .now() + 30) { [weak webView] in
        if webView?.isLoading == true {
            webView?.stopLoading()
            context.coordinator.onError("Page load timeout")
        }
    }
    
    webView.load(URLRequest(url: url))
    return webView
}

Automated Checks

🔒 External Domain URL Detected (Non-blocking)
URL: https://example.com
File: Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift:110

This URL appears in a preview/debug context only. Verify this is intentional for testing purposes.


Note: Unable to run build, tests, or linters in READ-ONLY review mode. These should be validated locally or via CI.

Show a centered ProgressView with "Loading..." text while the IC
page is loading. The loader is hidden once the page finishes loading.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add support for prefilling IC WebView form fields based on schema's
required fields. Encodes user data (fullName, dob) as Base64 JSON
and appends as prefill query parameter to WebView URL.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds deprecation annotation to CollectDataAction.fields property.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
jakubuid and others added 3 commits February 9, 2026 09:04
- Make WebView full screen, respect top safe area for notch
- Add close button to WebView
- Open Terms/Privacy links in Safari
- Move verified badge to merchant icon
- Add pobAddress to prefill fields
- Persist user-entered form data when going back
- Fix back navigation from confirmation to WebView

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Resolved conflict in PayPresenter.swift goBack() - kept WebView
navigation while adopting dismiss behavior from develop.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
4.6% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@jakubuid jakubuid merged commit 1777ede into develop Feb 11, 2026
10 of 13 checks passed
@jakubuid jakubuid deleted the feat/ic_webview branch February 11, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant