From e482afbd3fae1e006cd123dc487cfb25a488108b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 Apr 2026 20:47:42 -0500 Subject: [PATCH] Fix sandbox origin isolation and Android build issues --- .../java/pub/ditto/app/SandboxPlugin.java | 10 ++-- ios/App/App/SandboxPlugin.swift | 7 ++- src/components/SandboxFrame.tsx | 49 +++++++++++-------- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/android/app/src/main/java/pub/ditto/app/SandboxPlugin.java b/android/app/src/main/java/pub/ditto/app/SandboxPlugin.java index fa3f6f11..557792b2 100644 --- a/android/app/src/main/java/pub/ditto/app/SandboxPlugin.java +++ b/android/app/src/main/java/pub/ditto/app/SandboxPlugin.java @@ -13,7 +13,7 @@ import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; -import android.widget.FrameLayout; +import androidx.coordinatorlayout.widget.CoordinatorLayout; import com.getcapacitor.JSObject; import com.getcapacitor.Plugin; @@ -80,9 +80,11 @@ public class SandboxPlugin extends Plugin { sandboxes.put(sandboxId, sandbox); // Add the WebView on top of the Capacitor WebView. + // The parent is a CoordinatorLayout — using the wrong LayoutParams + // type causes a ClassCastException when it intercepts touch events. View capWebView = getBridge().getWebView(); ViewGroup parent = (ViewGroup) capWebView.getParent(); - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(pxWidth, pxHeight); + CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(pxWidth, pxHeight); params.leftMargin = pxX; params.topMargin = pxY; parent.addView(sandbox.webView, params); @@ -126,7 +128,7 @@ public class SandboxPlugin extends Plugin { return; } - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(pxWidth, pxHeight); + CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(pxWidth, pxHeight); params.leftMargin = pxX; params.topMargin = pxY; sandbox.webView.setLayoutParams(params); @@ -385,7 +387,7 @@ public class SandboxPlugin extends Plugin { " onMessage: function(data) {" + " var event = {" + " data: data," + - " origin: 'sandbox://app'," + + " origin: 'https://" + sandbox.id + ".sandbox.native'," + " source: window.parent," + " type: 'message'" + " };" + diff --git a/ios/App/App/SandboxPlugin.swift b/ios/App/App/SandboxPlugin.swift index 31ae1211..9dad365a 100644 --- a/ios/App/App/SandboxPlugin.swift +++ b/ios/App/App/SandboxPlugin.swift @@ -194,10 +194,9 @@ private class SandboxInstance: NSObject, WKScriptMessageHandler { self.id = id self.plugin = plugin - // Use a shortened ID for the scheme (URL schemes have length limits - // and must start with a letter). We use "sbx-" prefix + first 12 chars. - let schemeId = String(id.prefix(12)) - self.customScheme = "sbx-\(schemeId)" + // Each sandbox gets a unique custom URL scheme so that WKWebView + // assigns a distinct origin, isolating localStorage/IndexedDB/cookies. + self.customScheme = "sbx-\(id)" self.schemeHandler = SandboxSchemeHandler( sandboxId: id, diff --git a/src/components/SandboxFrame.tsx b/src/components/SandboxFrame.tsx index e46b84da..9bbe11e9 100644 --- a/src/components/SandboxFrame.tsx +++ b/src/components/SandboxFrame.tsx @@ -413,7 +413,34 @@ const SandboxFrameNative = forwardRef( const rect = el.getBoundingClientRect(); - // Create the native WebView. + // Register listeners BEFORE creating the WebView. On Android, + // `shouldInterceptRequest` fires on a background thread as soon + // as the WebView starts loading — if the fetch listener isn't + // registered yet, the event is lost and the request times out + // (the thread blocks via CountDownLatch waiting for a response + // that never arrives). + const fetchListener = await SandboxPlugin.addListener( + 'fetch', + (event: SandboxFetchEvent) => { + if (event.id !== id) return; + handleNativeFetch(event); + }, + ); + listeners.push(fetchListener); + + const scriptListener = await SandboxPlugin.addListener( + 'scriptMessage', + (event: SandboxScriptMessageEvent) => { + if (event.id !== id) return; + handleNativeScriptMessage(event); + }, + ); + listeners.push(scriptListener); + + if (cancelled || destroyedRef.current) return; + + // Create the native WebView. Fetch events from the initial load + // will be handled by the listeners registered above. await SandboxPlugin.create({ id, frame: { @@ -431,26 +458,6 @@ const SandboxFrameNative = forwardRef( } createdRef.current = true; - - // Listen for fetch requests from the native URL scheme handler. - const fetchListener = await SandboxPlugin.addListener( - 'fetch', - (event: SandboxFetchEvent) => { - if (event.id !== id) return; - handleNativeFetch(event); - }, - ); - listeners.push(fetchListener); - - // Listen for script messages (custom JSON-RPC from injected scripts). - const scriptListener = await SandboxPlugin.addListener( - 'scriptMessage', - (event: SandboxScriptMessageEvent) => { - if (event.id !== id) return; - handleNativeScriptMessage(event); - }, - ); - listeners.push(scriptListener); } // ---------------------------------------------------------------