Skip to content

[Bug] [MacOS] Changing location.hash breaks the JS‑Kotlin bridge #42

@mtpdog

Description

@mtpdog

Hi! We try to migrate to this lib but faced an issue with MacOS (JVM) target.
Here is the core issue found after some extensive debug work:

Summary

When the WebView changes location.hash (for example, using hash: true option in some map engines, or manually setting location.hash), the kmpJsBridge stops working on macOS. Subsequent calls to callNative time out (callback never invoked).
The issue does not occur on Android or iOS – only on macOS (Windows and Linux are not tested yet).

Steps to Reproduce

  1. Save the following HTML and load it in the Compose WebView on macOS:
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Bridge Diagnostic – Hash Change Bug</title>
    <style>
        body { margin: 0; font-family: sans-serif; }
        #debug {
            position: absolute; bottom: 10px; left: 10px; right: 10px;
            background: rgba(0,0,0,0.8); color: #0f0; font-family: monospace;
            font-size: 12px; padding: 8px; border-radius: 8px; z-index: 10;
            max-height: 200px; overflow-y: auto; pointer-events: none;
        }
        .buttons {
            position: absolute; top: 20px; right: 20px; z-index: 20;
            display: flex; gap: 10px; pointer-events: auto;
        }
        button {
            padding: 8px 16px; border: none; border-radius: 6px;
            cursor: pointer; font-weight: bold;
        }
        #callBtn { background: #007cbf; color: white; }
        #hashBtn { background: #f90; color: black; }
    </style>
</head>
<body>
    <div id="debug"></div>
    <div class="buttons">
        <button id="callBtn">📞 Call Bridge</button>
        <button id="hashBtn">🔁 Change hash</button>
    </div>

    <script>
        const debugDiv = document.getElementById('debug');
        function log(msg) {
            const line = `[${'$'}{new Date().toLocaleTimeString()}] ${'$'}{msg}`;
            console.log(line);
            debugDiv.innerHTML += line + '<br>';
            debugDiv.scrollTop = debugDiv.scrollHeight;
        }

        // Helper: bridge call with timeout
        function callNativeWithTimeout(method, params, onSuccess, onError) {
            if (!window.kmpJsBridge || typeof window.kmpJsBridge.callNative !== 'function') {
                onError("Bridge or callNative missing");
                return;
            }
            let called = false;
            const timeoutId = setTimeout(() => {
                if (!called) {
                    called = true;
                    onError("Native callback never fired (timeout 3s)");
                }
            }, 3000);

            try {
                window.kmpJsBridge.callNative(method, params, function(response) {
                    if (called) return;
                    called = true;
                    clearTimeout(timeoutId);
                    onSuccess(response);
                });
            } catch (e) {
                clearTimeout(timeoutId);
                onError(`Exception: ${'$'}{e.message}`);
            }
        }

        // Test bridge before any hash change
        log("🟢 Page loaded. Testing bridge (before hash change)...");
        callNativeWithTimeout("echo", { step: "initial" }, (res) => {
            log(`✅ Initial call SUCCESS: ${'$'}{res}`);
        }, (err) => {
            log(`❌ Initial call FAILED: ${'$'}{err}`);
        });

        // Listen to hash changes (this is what breaks the bridge on macOS)
        window.addEventListener('hashchange', () => {
            log(`⚠️ Hash changed to: ${'$'}{location.hash}`);
            log("Testing bridge AFTER hash change...");
            callNativeWithTimeout("echo", { step: "after-hash" }, (res) => {
                log(`✅ After-hash call SUCCESS: ${'$'}{res}`);
            }, (err) => {
                log(`❌ After-hash call FAILED: ${'$'}{err} ← BRIDGE IS BROKEN`);
            });
        });

        // Manual bridge call button
        document.getElementById('callBtn').addEventListener('click', () => {
            log("🔘 Manual bridge call...");
            callNativeWithTimeout("echo", { manual: true }, (res) => {
                log(`✅ Manual call SUCCESS: ${'$'}{res}`);
            }, (err) => {
                log(`❌ Manual call FAILED: ${'$'}{err}`);
            });
        });

        // Button to change hash (triggers the bug on macOS)
        document.getElementById('hashBtn').addEventListener('click', () => {
            const newHash = "#test-" + Date.now();
            log(`🔄 Changing hash to: ${'$'}{newHash}`);
            location.hash = newHash;
        });

        // Expose helper for Kotlin if needed
        window.jsEcho = function(msg) {
            log(`📨 Kotlin sent: ${'$'}{msg}`);
            return "JS received: " + msg;
        };
    </script>
</body>
</html>
  1. Kotlin composable (common code):
@Composable
fun App() {
    val state = rememberWebViewState("http://localhost:8080/test.html") // serve the HTML
    val navigator = rememberWebViewNavigator()
    val jsBridge = rememberWebViewJsBridge(navigator)

    jsBridge.register {
        object : IJsMessageHandler {
            override fun methodName() = "echo"
            override fun handle(message: JsMessage, navigator: WebViewNavigator?, callback: (String) -> Unit) {
                println("Native received: ${message.params}")
                callback("OK")
            }
        }
    }

    WebView(state = state, navigator = navigator)
}

Steps to reproduce on macOS

  1. Run the app on macOS.
  2. Observe that Before hash change succeeds (callback logged).
  3. Click the “Change hash” button.
  4. Observe that After hash change times out (callback never invoked).
kmpbridge.mov

On Android/iOS: both calls succeed.

Expected behavior

Changing the URL hash should not affect the bridge.

Possible cause

The macOS backend (JavaFX WebView) may treat hash changes as a navigation event that resets the JavaScript context or detaches the injected native bridge object. The library likely needs to re‑inject the bridge after hash changes or use a more robust attachment method.

Workaround for users

Avoid changing location.hash on macOS

Request

Could you please investigate the macOS bridge implementation to ensure it survives hashchange events? This would make the bridge reliable for all pages that modify the URL fragment.

Thank you for your excellent work on this library!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions