feat(pay): add WebView-based Information Capture for Pay SDK#254
feat(pay): add WebView-based Information Capture for Pay SDK#254
Conversation
- 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>
…to feat/ic_webview
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 finished @jakubuid's task in 1m 43s —— View job Code Review CompleteReviewing PR #254: WebView-based Information Capture for Pay SDK Issue 1: Missing URL validation before loading in WebViewID: paydatacoll-url-validation-a7c3 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 restrictionsID: paydatacoll-js-injection-b8f1 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 = falseIssue 3: Missing TLS/SSL certificate validationID: paydatacoll-ssl-validation-c2d4 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 restrictionsID: paydatacoll-nav-policy-e5f8 Coordinator doesn't implement 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 cleanupID: paydatacoll-handler-leak-f1a9 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 validationID: paydatacoll-origin-check-d3b7 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 logsID: paypresenter-sensitive-logs-a8c2 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)")
#endifIssue 8: Missing validation for IC completion success flagID: paydatacoll-success-validation-b2c8 When 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 formattingID: paysuccessvi-divide-zero-c9d4 If 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 loadID: paydatacoll-load-timeout-e1f2 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) 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. |
Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift
Show resolved
Hide resolved
Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift
Show resolved
Hide resolved
Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift
Show resolved
Hide resolved
Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift
Show resolved
Hide resolved
Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift
Show resolved
Hide resolved
Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift
Show resolved
Hide resolved
Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift
Show resolved
Hide resolved
Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift
Show resolved
Hide resolved
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>
- 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>
|


Summary
Add WebView-based Information Capture (IC) for the payment flow. When
collectDataAction.urlis 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
collectData.urlis present, load WebView instead of native form fieldspayDataCollectionCompletemessage handler for WebView → Wallet communicationwebviewDataCollectionstep toPayFlowStepenumFiles Changed
Package.swiftPayPresenter.swiftPayDataCollectionWebView.swiftPayContainerView.swiftPaySuccessView.swiftJavaScript Bridge Protocol
The IC page communicates via:
Flow Diagram
Test plan
collectData.url🤖 Generated with Claude Code