capacitor-plugins icon indicating copy to clipboard operation
capacitor-plugins copied to clipboard

Defer "resume" event in AppPlugin until WebView is ready (prevents JS eval errors after crash)

Open smart opened this issue 9 months ago • 0 comments

After going down the rabbit hole with AI ⸻

Bug Report

Plugin(s)

@capacitor/app

Capacitor Version

❯ npx cap doctor 💊 Capacitor Doctor 💊

Capacitor CLI: 7.x.x @capacitor/core: 7.x.x @capacitor/ios: 7.x.x @capacitor/android: not used Installed Dependencies: @capacitor/app: 7.x.x

Platform(s) • iOS (likely only relevant to iOS since it’s WKWebView related)

Current Behavior

When the app resumes from the background and the WKWebView process was previously killed by the OS (e.g. due to memory pressure), the AppPlugin immediately tries to notifyListeners("resume", ...) — triggering JavaScript evaluation before the WebView has finished reloading.

This results in:

⚡️ JS Eval error A JavaScript exception occurred 🕵️ JS EVAL ATTEMPT: window.Capacitor.triggerEvent('resume', 'document') ⚠️ JS called while WKWebView is still loading

Because the WebView is not yet ready, this call is ineffective and noisy. Worse, it may trigger app logic relying on a fully functional DOM, even though the page hasn’t yet rendered.

Expected Behavior

The "resume" event should not be sent until the WKWebView is fully loaded and ready to execute JS safely. This would mirror how other platform events defer firing until the bridge is available.

Code Reproduction

Not easily reproducible with a clean project, but you can simulate the crash scenario like this: 1. Load your app in Simulator. 2. Push it to background. 3. Manually kill the WebView process (or let the OS do it under pressure). 4. Resume the app. 5. Observe the JavaScript error and stack trace in the Xcode logs.

Alternatively, set up a logging swizzle like this to confirm JS is being evaluated too early:

extension WKWebView { @objc func swizzled_evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) { print("🕵️ JS EVAL ATTEMPT: (javaScriptString.prefix(100))") if self.isLoading { print("⚠️ JS called while WKWebView is still loading") } else { print("✅ WebView is ready") } swizzled_evaluateJavaScript(javaScriptString, completionHandler: completionHandler) } }

Other Technical Details • WKWebView restarts cleanly • applicationWillEnterForeground + applicationDidBecomeActive used to re-add splash UI while WebView is reloading • window.Capacitor.fromNative(...) starts firing while page hasn’t re-rendered

Additional Context

A possible solution would be to defer the "resume" event in AppPlugin.load() by checking the bridge’s state and using something like:

self.bridge?.onReady { self?.notifyListeners("resume", data: nil) }

Proposed solution Here’s a ready-to-paste proposed patch you can include in your GitHub issue (or use to open a PR later):

🔧 Proposed Patch (AppPlugin.swift)

// Inside AppPlugin.load()

observers.append(NotificationCenter.default.addObserver( forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main ) { [weak self] (_) in guard let self = self else { return }

// Defer "resume" until bridge is ready
if let bridge = self.bridge, bridge.isReady {
    self.notifyListeners("resume", data: nil)
} else {
    self.bridge?.onReady {
        self.notifyListeners("resume", data: nil)
    }
}

})

✅ Why This Fix Helps • Prevents firing window.Capacitor.triggerEvent('resume', 'document') while the WebView is still reloading. • Eliminates console noise and misleading errors. • Aligns with Capacitor’s general behavior of deferring plugin JS events until bridge is ready.

smart avatar May 02 '25 20:05 smart