stripe / stripe-js

Loading wrapper for Stripe.js
https://stripe.com/docs/js
MIT License
622 stars 153 forks source link

[BUG]: Stripe.js freezes iOS WKWebView on navigation after visiting external link #614

Closed serebrov closed 1 month ago

serebrov commented 4 months ago

What happened?

The issue is reproduced in capacitor-based native iOS application. Capacitor wraps web application into the native app and the original web application is running inside the native inside the WKWebView.

In our case, this is a Vue.js web application and we have the <script src="https://js.stripe.com/v3/"></script> line in our main index.html file.

The application is a content reader where the content may include links to external web sites. The issue is reproduced like this:

Result: navigation does not work, the app stays on the same page. Overall, the app hangs and does not respond to any input, the device starts heating.

This is only reproduced in WKWebView. The same web app works well in Mobile Safari, also no issues on other platforms.

Workarounds:

Environment

WKWebView on iOS 17.5, 17.0, 16.2, possibly other versions too

Reproduction

No response

serebrov commented 4 months ago

I am mostly filing this issue for information. It took me quite some time to find the reason for the bug and it may help other people having this issue. So I think it is fine to just close it if it does not make sense for Stripe.js developers to fix it, should be a rare edge case.

serebrov commented 4 months ago

Also, for reference, code examples for workarounds.

Remove the <script src="https://js.stripe.com/v3/"></script> from HTML, dynamically load Stripe in the app code:

import { STRIPE_PUBLIC_KEY } from '@/init/settings'
// The `platform` is a wrapper around Capacitor API, using the Device.getInfo() method
// see https://capacitorjs.com/docs/apis/device#getinfo
import { platform } from '@/services/platform'

// Call initStripe somewhere in the app initialization code
export async function initStripe() {
  const isIOS = await platform.isNativeIOSApp()

  if (isIOS) {
    // Stripe.js freezes the native iOS app after visiting the external link, skip the import.
    return
  }
  // Note: we can not use normal import, `import { loadStripe } from '@stripe/stripe-js'` 
  // as it still triggers the same problem.
  // Dynamic import (https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import) works:
  const stripe = await import('@stripe/stripe-js')
  await stripe.loadStripe(STRIPE_PUBLIC_KEY)
}

Alternative solution, block Stripe.js from loading in the native code:

import UIKit
import Capacitor

// This overrides the default Capacitor controller
// See: https://capacitorjs.com/docs/ios/viewcontroller

class MyViewController: CAPBridgeViewController {

    override func webView(with frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView {
        let webview = WKWebView(frame: frame, configuration: configuration)

        // Configure webview to block Stripe.js
        // The script is loaded in index.app.html and it causes an issue with
        // navigation in webview: after visiting the extermal link, the next
        // in-app navigation freezes the UI.
        //
        // The implementation is based on the example here:
        // https://stackoverflow.com/a/48084455
        let blockRules =
            """
        [{
             "trigger": {
                 "url-filter": "js.stripe.com/v3/*",
                 "resource-type": ["script"]
             },
             "action": {
                 "type": "block"
             }
         }]
        """

        // Swift note: the method we call below, `compileContentRuleList`, is
        // asynchronus, we pass a handler that gets parsed rules and
        // adds them to the webview configuration.
        // Note that the handler is passed after the method call, as a block
        // in curly braces. This is called a trailing closure, see
        // https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Trailing-Closures
        // It is a syntax sugar, the handler is actually an argument
        // of the `compileContentRuleList` method.
        WKContentRuleListStore.default()
            .compileContentRuleList(forIdentifier: "ContentBlockingRules",
                                    encodedContentRuleList: blockRules)
        { (contentRuleList, error) in
            guard let contentRuleList = contentRuleList,
                error == nil else {
                return
            }

            let configuration = webview.configuration
            configuration.userContentController.add(contentRuleList)
        }

        return webview

    }
}
brendanm-stripe commented 3 months ago

I'm glad you found a workaround, but this sounds bizarre. While we have no official support for capacitor, we do expect Stripe.js to work within web views using the system-provided webkit engige: supported browsers.

I know you said the device locks up, but if you're able to get any logs from the device or browser to share, perhaps that would help identify where this problem originates.

Separately, and alternative import patter you could try is the /pure import (doc) where you control when the Stripe.js resource is loaded:

// ES module import
import {loadStripe} from '@stripe/stripe-js/pure';

// Stripe.js will not be loaded until `loadStripe` is called
const stripe = await loadStripe('pk_test_123');

(instead of your dynamic const stripe = await import('@stripe/stripe-js'))

serebrov commented 3 months ago

@brendanm-stripe Thanks for the information and the /pure tip, it looks better than the dynamic import I came up with!

Unfortunately, there are no logs from the device. The web view just freezes and there is no output in the web console nor in the device log shown in XCode.

To be more specific, "freezes" means that I can still scroll the content in the web view, but none of the links work and it stops rendering the content beyond what was already on the screen (I see some text, if I scroll up or down, the page is just blank). The device also starts heating up quickly, so it feels like there is an infinite loop somewhere in the link click handler or maybe in the WKWebView itself.

stale[bot] commented 2 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.