diff --git a/payments/wallets/webview.mdx b/payments/wallets/webview.mdx index 02e8cd7..da42d04 100644 --- a/payments/wallets/webview.mdx +++ b/payments/wallets/webview.mdx @@ -165,7 +165,14 @@ export function PayWebView({gatewayUrl, appDeepLink, onSuccess, onFailure}: PayW Linking.openURL(request.url).catch(console.warn); return false; } - return true; + // Allow https (the checkout) and about:; block any other scheme so the + // page can't drive the OS into arbitrary native schemes (tel:, sms:, …). + try { + const scheme = new URL(request.url).protocol; + return scheme === 'https:' || scheme === 'about:'; + } catch { + return false; + } }, [], ); @@ -208,6 +215,7 @@ const styles = StyleSheet.create({webview: {flex: 1}}); ```kotlin import android.content.ActivityNotFoundException +import android.content.Context import android.content.Intent import android.net.Uri import android.os.Handler @@ -232,6 +240,24 @@ fun isWalletDeeplink(url: String): Boolean = try { Uri.parse(url).getQueryParameter("uri")?.startsWith("wc:") == true } catch (_: Exception) { false } +// Shared by both shouldOverrideUrlLoading overloads. Returns true = handled (don't load +// in the WebView), false = let the WebView load it. Forward wc: wallet deeplinks to the OS; +// allow https (the checkout) and about: to load; block any other scheme (tel:, sms:, +// intent:, …) so the page can't drive the OS into arbitrary native apps. +private fun handleWalletDeeplink(context: Context, url: String?): Boolean { + if (url == null) return false + if (isWalletDeeplink(url)) { + try { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + } catch (_: ActivityNotFoundException) { + // No app can handle the wallet deeplink (wallet not installed). + } + return true + } + val scheme = Uri.parse(url).scheme?.lowercase() + return scheme != "https" && scheme != "about" +} + @Composable fun PayWebView( gatewayUrl: String, @@ -274,22 +300,16 @@ fun PayWebView( "ReactNativeWebView" ) webViewClient = object : WebViewClient() { - // This WebResourceRequest overload is API 24+. If you support minSdk < 24, - // also override the deprecated shouldOverrideUrlLoading(view, url: String) and - // run the same isWalletDeeplink check — otherwise deeplink interception - // silently no-ops on API 23. - override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { - val reqUrl = request?.url?.toString() ?: return false - if (isWalletDeeplink(reqUrl)) { - try { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(reqUrl))) - } catch (_: ActivityNotFoundException) { - // No app can handle the wallet deeplink (wallet not installed). - } - return true - } - return false - } + // API 24+ overload. + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean = + handleWalletDeeplink(context, request?.url?.toString()) + + // Deprecated pre-API-24 overload — still the only one called on API 23, so + // without it deeplink interception silently no-ops there and wc: links would + // load inside the WebView instead of opening the wallet. + @Suppress("DEPRECATION") + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean = + handleWalletDeeplink(context, url) } loadUrl(buildPayUrl(gatewayUrl, appDeepLink)) } @@ -458,7 +478,13 @@ class _PayWebViewState extends State { } return NavigationDecision.prevent; } - return NavigationDecision.navigate; + // Allow https (the checkout) and about:; block any other scheme so the + // page can't drive the OS into arbitrary native schemes (tel:, sms:, …). + final scheme = Uri.tryParse(request.url)?.scheme.toLowerCase(); + if (scheme == 'https' || scheme == 'about') { + return NavigationDecision.navigate; + } + return NavigationDecision.prevent; }, )) ..addJavaScriptChannel('ReactNativeWebView', onMessageReceived: (msg) {