diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 80a064c68a..ea338db0f9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,7 +21,7 @@ concurrency:
jobs:
setup:
runs-on: ubuntu-latest
- timeout-minutes: 10
+ timeout-minutes: 30
steps:
- name: Get Latest
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -38,7 +38,7 @@ jobs:
key: ${{ runner.OS }}-bun-dependencies-cache-${{ hashFiles('bun.lock') }}
lint:
runs-on: macos-15
- timeout-minutes: 10
+ timeout-minutes: 30
steps:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
@@ -57,7 +57,7 @@ jobs:
- run: bun run lint
test-cli:
runs-on: macos-15
- timeout-minutes: 10
+ timeout-minutes: 30
needs:
- setup
- lint
@@ -81,7 +81,7 @@ jobs:
working-directory: ./cli
test-core:
runs-on: ubuntu-latest
- timeout-minutes: 10
+ timeout-minutes: 30
needs:
- setup
- lint
@@ -105,7 +105,7 @@ jobs:
working-directory: ./core
test-ios:
runs-on: macos-15
- timeout-minutes: 10
+ timeout-minutes: 30
needs:
- setup
- lint
@@ -136,7 +136,7 @@ jobs:
run: sh ./scripts/native-podspec.sh lint
test-android:
runs-on: ubuntu-latest
- timeout-minutes: 10
+ timeout-minutes: 30
needs:
- setup
- lint
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5064d193a9..5fd3394cf1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,33 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-## [8.5.2](https://github.com/Cap-go/capacitor-plus/compare/8.5.1...8.5.2) (2026-08-26)
-
-**Note:** Version bump only for package capacitor
-
-
-
-
-
-## [8.5.1](https://github.com/Cap-go/capacitor-plus/compare/8.3.12...8.5.1) (2026-08-25)
-
+## [8.5.1](https://github.com/ionic-team/capacitor/compare/8.5.0...8.5.1) (2026-08-31)
### Bug Fixes
-* **ci:** fetch upstream objects so plus branch sync works ([#109](https://github.com/Cap-go/capacitor-plus/issues/109)) ([1fa7eee](https://github.com/Cap-go/capacitor-plus/commit/1fa7eeeff9494e9b32afb20a9850389b79d6bcec))
-* **ci:** resolve modify/delete conflicts in sync conflict PRs ([#110](https://github.com/Cap-go/capacitor-plus/issues/110)) ([5c29fb9](https://github.com/Cap-go/capacitor-plus/commit/5c29fb9af001b9aafdcde7b7054585e093f44431))
-
-
-
-
+- block navigation to the internal HTTP proxy path ([ee586ae](https://github.com/ionic-team/capacitor/commit/ee586ae680887ba99d066616f976db149542d922))
+- **cli:** use POSIX paths in CapApp-SPM Package.swift ([#8549](https://github.com/ionic-team/capacitor/issues/8549)) ([5e5bb3b](https://github.com/ionic-team/capacitor/commit/5e5bb3befc312477900252ab07e23b596f8cb0d1))
+- **core:** prevent removeListener from removing wrong listener ([#8271](https://github.com/ionic-team/capacitor/issues/8271)) ([5ac4dd6](https://github.com/ionic-team/capacitor/commit/5ac4dd613ae989d8dc8738ea25b77efbd4fa21fe))
# [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31)
-## Bug Fixes
+### Bug Fixes
- **cli:** support TypeScript 7 when loading capacitor.config.ts ([#8534](https://github.com/ionic-team/capacitor/issues/8534)) ([4c1c870](https://github.com/ionic-team/capacitor/commit/4c1c8709413b9c19b008c99122ca330cc3c90e6f))
-## Features
+### Features
- **cli:** add migrator functionality for adopting UIScene ([#8544](https://github.com/ionic-team/capacitor/issues/8544)) ([984fa85](https://github.com/ionic-team/capacitor/commit/984fa85ba0adab0aacf895aed6323bf4b503dccb))
- **ios:** UIScene Support ([#8536](https://github.com/ionic-team/capacitor/issues/8536)) ([3fa04a3](https://github.com/ionic-team/capacitor/commit/3fa04a357c92af34cd6fccb8124791963804a9dc))
@@ -49,14 +37,14 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02)
-## Bug Fixes
+### Bug Fixes
- **android:** show only the requested system bar ([#8480](https://github.com/ionic-team/capacitor/issues/8480)) ([4c6c321](https://github.com/ionic-team/capacitor/commit/4c6c3219afb5223211e857457e46283c37eb9424))
- **cli:** revert live reload config on failure ([#8485](https://github.com/ionic-team/capacitor/issues/8485)) ([1d031a4](https://github.com/ionic-team/capacitor/commit/1d031a4abec2c793079ba8897ad2e40c4cc6c7f9))
- **SystemBars:** make `safe-area-inset-x` available on API <= 34 ([#8424](https://github.com/ionic-team/capacitor/issues/8424)) ([e456de0](https://github.com/ionic-team/capacitor/commit/e456de083e19644f484bec5a5359cb67960ac8bc))
- **SystemBars:** respect `insetsHandling` disable ([#8481](https://github.com/ionic-team/capacitor/issues/8481)) ([d4ad7ff](https://github.com/ionic-team/capacitor/commit/d4ad7ffe39daf66e0cfc63af9028d5c05543bde7))
-## Features
+### Features
- add method getDouble to plugin config ([#7638](https://github.com/ionic-team/capacitor/issues/7638)) ([93c72de](https://github.com/ionic-team/capacitor/commit/93c72de40a2ec4c78b33659250cb08340083088e))
- **cli:** add experimental packageOptions ([#8471](https://github.com/ionic-team/capacitor/issues/8471)) ([258867b](https://github.com/ionic-team/capacitor/commit/258867b7bf37b1837b99b02ec9638e5a6df08d97))
diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md
index a6ec97e3ba..54fa1f0fd1 100644
--- a/android/CHANGELOG.md
+++ b/android/CHANGELOG.md
@@ -3,21 +3,11 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-## [8.5.2](https://github.com/Cap-go/capacitor-plus/compare/8.5.1...8.5.2) (2026-08-26)
-
-**Note:** Version bump only for package @capacitor-plus/android
-
-
-
-
-
-## [8.5.1](https://github.com/Cap-go/capacitor-plus/compare/8.3.12...8.5.1) (2026-08-25)
-
-**Note:** Version bump only for package @capacitor-plus/android
-
-
+## [8.5.1](https://github.com/ionic-team/capacitor/compare/8.5.0...8.5.1) (2026-08-31)
+### Bug Fixes
+- block navigation to the internal HTTP proxy path ([ee586ae](https://github.com/ionic-team/capacitor/commit/ee586ae680887ba99d066616f976db149542d922))
# [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31)
diff --git a/android/capacitor/src/androidTest/AndroidManifest.xml b/android/capacitor/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000000..33952f2e02
--- /dev/null
+++ b/android/capacitor/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/android/capacitor/src/androidTest/java/com/getcapacitor/android/HttpInterceptorNavigationTest.java b/android/capacitor/src/androidTest/java/com/getcapacitor/android/HttpInterceptorNavigationTest.java
new file mode 100644
index 0000000000..1cde86b4ba
--- /dev/null
+++ b/android/capacitor/src/androidTest/java/com/getcapacitor/android/HttpInterceptorNavigationTest.java
@@ -0,0 +1,115 @@
+package com.getcapacitor.android;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.webkit.WebResourceRequest;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.getcapacitor.Bridge;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * The proxy path shares the app's host and scheme, so the host/scheme guard alone would let it
+ * load in the WebView.
+ */
+@RunWith(AndroidJUnit4.class)
+public class HttpInterceptorNavigationTest {
+
+ private static final String INTERCEPTOR_URL =
+ "https://localhost" + Bridge.CAPACITOR_HTTP_INTERCEPTOR_START + "?u=https://example.com/payload.html";
+ private static final String IN_APP_URL = "https://localhost/index.html";
+ private static final String EXTERNAL_URL = "https://example.com/";
+
+ /** A plugin registered by the test host returns "allow" for the proxy path; it must not win. */
+ @Test
+ public void blocksNavigationToInterceptorPath() {
+ try (ActivityScenario scenario = ActivityScenario.launch(TestHostActivity.class)) {
+ scenario.onActivity((activity) -> {
+ Bridge bridge = activity.getBridge();
+ assertNotNull(bridge);
+ assertTrue("interceptor navigation must be blocked", bridge.launchIntent(Uri.parse(INTERCEPTOR_URL)));
+ assertFalse("in-app navigation must stay in the WebView", bridge.launchIntent(Uri.parse(IN_APP_URL)));
+ assertTrue("external navigation must leave the WebView", bridge.launchIntent(Uri.parse(EXTERNAL_URL)));
+ });
+ }
+ }
+
+ /** An iframe looks like a fetch to isForMainFrame(), so subframe documents must be refused too. */
+ @Test
+ public void refusesProxyForDocumentRequests() {
+ try (ActivityScenario scenario = ActivityScenario.launch(TestHostActivity.class)) {
+ scenario.onActivity((activity) -> {
+ Bridge bridge = activity.getBridge();
+ assertNotNull(bridge);
+ // Without this the proxy refuses everything and the assertions below prove nothing.
+ assertTrue(
+ "CapacitorHttp must be enabled for this test to mean anything",
+ bridge.getConfig().getPluginConfiguration("CapacitorHttp").getBoolean("enabled", false)
+ );
+
+ Map navHeaders = new HashMap<>();
+ navHeaders.put("Accept", "text/html,application/xhtml+xml");
+ navHeaders.put("Upgrade-Insecure-Requests", "1");
+
+ assertNull(
+ "main frame document must be refused",
+ bridge.getLocalServer().shouldInterceptRequest(new FakeRequest(INTERCEPTOR_URL, true, navHeaders))
+ );
+ assertNull(
+ "iframe document must be refused",
+ bridge.getLocalServer().shouldInterceptRequest(new FakeRequest(INTERCEPTOR_URL, false, navHeaders))
+ );
+ });
+ }
+ }
+
+ private static class FakeRequest implements WebResourceRequest {
+
+ private final Uri url;
+ private final boolean mainFrame;
+ private final Map headers;
+
+ FakeRequest(String url, boolean mainFrame, Map headers) {
+ this.url = Uri.parse(url);
+ this.mainFrame = mainFrame;
+ this.headers = headers;
+ }
+
+ @Override
+ public Uri getUrl() {
+ return url;
+ }
+
+ @Override
+ public boolean isForMainFrame() {
+ return mainFrame;
+ }
+
+ @Override
+ public boolean isRedirect() {
+ return false;
+ }
+
+ @Override
+ public boolean hasGesture() {
+ return false;
+ }
+
+ @Override
+ public String getMethod() {
+ return "GET";
+ }
+
+ @Override
+ public Map getRequestHeaders() {
+ return headers;
+ }
+ }
+}
diff --git a/android/capacitor/src/androidTest/java/com/getcapacitor/android/InterceptorAllowingPlugin.java b/android/capacitor/src/androidTest/java/com/getcapacitor/android/InterceptorAllowingPlugin.java
new file mode 100644
index 0000000000..49be9f906a
--- /dev/null
+++ b/android/capacitor/src/androidTest/java/com/getcapacitor/android/InterceptorAllowingPlugin.java
@@ -0,0 +1,20 @@
+package com.getcapacitor.android;
+
+import android.net.Uri;
+import com.getcapacitor.Bridge;
+import com.getcapacitor.Plugin;
+import com.getcapacitor.annotation.CapacitorPlugin;
+
+/** A plugin that tries to allow the proxy path. The navigation guard must ignore it. */
+@CapacitorPlugin(name = "InterceptorAllowingPlugin")
+public class InterceptorAllowingPlugin extends Plugin {
+
+ @Override
+ public Boolean shouldOverrideLoad(Uri url) {
+ String path = url.getPath();
+ if (path != null && path.startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START)) {
+ return false; // "allow this navigation"
+ }
+ return null;
+ }
+}
diff --git a/android/capacitor/src/androidTest/java/com/getcapacitor/android/TestHostActivity.java b/android/capacitor/src/androidTest/java/com/getcapacitor/android/TestHostActivity.java
new file mode 100644
index 0000000000..05d7a5d7c5
--- /dev/null
+++ b/android/capacitor/src/androidTest/java/com/getcapacitor/android/TestHostActivity.java
@@ -0,0 +1,27 @@
+package com.getcapacitor.android;
+
+import com.getcapacitor.BridgeActivity;
+import com.getcapacitor.CapConfig;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Host for instrumented tests. CapacitorHttp is on so the proxy is reachable, and a plugin that
+ * tries to allow the proxy path is registered so tests can prove the guard still wins.
+ */
+public class TestHostActivity extends BridgeActivity {
+
+ @Override
+ protected void load() {
+ registerPlugin(InterceptorAllowingPlugin.class);
+
+ try {
+ JSONObject plugins = new JSONObject("{\"CapacitorHttp\":{\"enabled\":true}}");
+ config = new CapConfig.Builder(this).setPluginsConfiguration(plugins).create();
+ } catch (JSONException e) {
+ throw new IllegalStateException("bad test plugin config", e);
+ }
+
+ super.load();
+ }
+}
diff --git a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java
index ca6fd9c8fc..65cefa4d39 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java
@@ -394,6 +394,12 @@ private int extractWebViewMajorVersion(final PackageManager pm, final String web
}
public boolean launchIntent(Uri url) {
+ // The proxy returns a remote body at the app origin, so block it before plugins can allow it.
+ String path = url.getPath();
+ if (path != null && path.startsWith(CAPACITOR_HTTP_INTERCEPTOR_START)) {
+ return true;
+ }
+
/*
* Give plugins the chance to handle the url
*/
diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
index c2075811a0..bac57209fa 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
@@ -412,12 +412,6 @@ private boolean showImageCapturePicker(final ValueCallback filePathCallba
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri);
takePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
-
- // Store in static variables to survive activity recreation
- pendingFilePathCallback = filePathCallback;
- pendingImageFileUri = imageFileUri;
- pendingFileChooserType = FileChooserType.IMAGE_CAPTURE;
-
activityListener = (activityResult) -> {
Uri[] result = null;
if (activityResult.getResultCode() == Activity.RESULT_OK) {
diff --git a/android/capacitor/src/main/java/com/getcapacitor/Plugin.java b/android/capacitor/src/main/java/com/getcapacitor/Plugin.java
index 19c8b8b3a6..e209bab8aa 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/Plugin.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/Plugin.java
@@ -1003,7 +1003,8 @@ protected void handleOnDestroy() {}
* Give the plugins a chance to take control when a URL is about to be loaded in the WebView.
* Returning true causes the WebView to abort loading the URL.
* Returning false causes the WebView to continue loading the URL.
- * Returning null will defer to the default Capacitor policy
+ * Returning null will defer to the default Capacitor policy.
+ * Not called for Capacitor's internal HTTP proxy path, which is always blocked.
*/
@SuppressWarnings("unused")
public Boolean shouldOverrideLoad(Uri url) {
diff --git a/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java b/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java
index 016609ce3a..8974173a23 100755
--- a/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java
@@ -185,6 +185,11 @@ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
Uri loadingUrl = request.getUrl();
if (null != loadingUrl.getPath() && loadingUrl.getPath().startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START)) {
+ // Only fetch/XHR should reach the proxy; a document would run remote content at the app origin.
+ boolean httpEnabled = bridge.getConfig().getPluginConfiguration("CapacitorHttp").getBoolean("enabled", false);
+ if (!httpEnabled || isDocumentRequest(request)) {
+ return null;
+ }
Logger.debug("Handling CapacitorHttp request: " + loadingUrl);
try {
return handleCapacitorHttpRequest(request);
@@ -210,6 +215,24 @@ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
}
}
+ /** isForMainFrame() is false for an iframe and a fetch alike; only navigations send this header. */
+ private boolean isDocumentRequest(WebResourceRequest request) {
+ if (request.isForMainFrame()) {
+ return true;
+ }
+ Map headers = request.getRequestHeaders();
+ if (headers == null) {
+ // The proxy needs the headers too, so the request fails there anyway.
+ return false;
+ }
+ for (String header : headers.keySet()) {
+ if ("Upgrade-Insecure-Requests".equalsIgnoreCase(header)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private boolean isLocalFile(Uri uri) {
String path = uri.getPath();
return path.startsWith(capacitorContentStart) || path.startsWith(capacitorFileStart);
@@ -333,6 +356,9 @@ private WebResourceResponse handleCapacitorHttpRequest(WebResourceRequest reques
int responseCode = connection.getResponseCode();
String reasonPhrase = getReasonPhraseFromResponseCode(responseCode);
+ // Nothing should render this. If anything does, sandbox keeps it inert and off the app origin.
+ responseHeaders.put("Content-Security-Policy", "sandbox; frame-ancestors 'none'");
+
return new WebResourceResponse(mimeType, encoding, responseCode, reasonPhrase, responseHeaders, inputStream);
}
@@ -756,48 +782,6 @@ public long skip(long n) throws IOException {
}
}
- /**
- * An InputStream wrapper that limits the number of bytes that can be read.
- */
- static class BoundedInputStream extends InputStream {
-
- private final InputStream in;
- private long remaining;
-
- public BoundedInputStream(InputStream in, long limit) {
- this.in = in;
- this.remaining = limit;
- }
-
- @Override
- public int available() throws IOException {
- int available = in.available();
- return (int) Math.min(available, remaining);
- }
-
- @Override
- public int read() throws IOException {
- if (remaining <= 0) return -1;
- int result = in.read();
- if (result != -1) remaining--;
- return result;
- }
-
- @Override
- public int read(byte[] b, int off, int len) throws IOException {
- if (remaining <= 0) return -1;
- int toRead = (int) Math.min(len, remaining);
- int result = in.read(b, off, toRead);
- if (result > 0) remaining -= result;
- return result;
- }
-
- @Override
- public void close() throws IOException {
- in.close();
- }
- }
-
// For L and above.
private static class LollipopLazyInputStream extends LazyInputStream {
diff --git a/android/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java b/android/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java
index 1115429d7b..020c1457a1 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java
@@ -70,14 +70,12 @@ public CapacitorEvalBridgeMode(WebView webView, CordovaInterface cordova) {
@Override
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
- cordova
- .getActivity()
- .runOnUiThread(() -> {
- String js = queue.popAndEncodeAsJs();
- if (js != null) {
- webView.evaluateJavascript(js, null);
- }
- });
+ cordova.getActivity().runOnUiThread(() -> {
+ String js = queue.popAndEncodeAsJs();
+ if (js != null) {
+ webView.evaluateJavascript(js, null);
+ }
+ });
}
}
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
index 84a4bfa8a3..9a50858f40 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
@@ -44,17 +44,17 @@ public class SystemBars extends Plugin {
private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_KEYBOARD_FIX = 144;
static final String viewportMetaJSFunction = """
- function capacitorSystemBarsCheckMetaViewport() {
- const meta = document.querySelectorAll("meta[name=viewport]");
- if (meta.length == 0) {
- return false;
- }
- // get the last found meta viewport tag
- const metaContent = meta[meta.length - 1].content;
- return metaContent.includes("viewport-fit=cover");
+ function capacitorSystemBarsCheckMetaViewport() {
+ const meta = document.querySelectorAll("meta[name=viewport]");
+ if (meta.length == 0) {
+ return false;
}
- capacitorSystemBarsCheckMetaViewport();
- """;
+ // get the last found meta viewport tag
+ const metaContent = meta[meta.length - 1].content;
+ return metaContent.includes("viewport-fit=cover");
+ }
+ capacitorSystemBarsCheckMetaViewport();
+ """;
private String insetsHandling = INSETS_HANDLING_CSS;
private boolean hasViewportCover = false;
@@ -272,7 +272,7 @@ private void initWindowInsetsListener() {
// We need to correct for a possible shown IME
v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0);
- Insets safeAreaInsets = calcSafeAreaInsets(safeAreaSource);
+ Insets safeAreaInsets = calcSafeAreaInsets(insets);
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
return new WindowInsetsCompat.Builder(insets)
@@ -297,7 +297,7 @@ private void initWindowInsetsListener() {
.setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0))
.build();
- Insets safeAreaInsets = calcSafeAreaInsets(safeAreaSource);
+ Insets safeAreaInsets = calcSafeAreaInsets(newInsets);
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
return newInsets;
@@ -364,7 +364,6 @@ private void setHidden(boolean hide, String bar) {
if (hide) {
if (bar.isEmpty()) {
windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars());
- navBarVisible = false;
} else if (bar.equals(BAR_STATUS_BAR)) {
windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.statusBars());
} else if (bar.equals(BAR_GESTURE_BAR)) {
@@ -376,7 +375,6 @@ private void setHidden(boolean hide, String bar) {
if (bar.isEmpty()) {
windowInsetsControllerCompat.show(WindowInsetsCompat.Type.systemBars());
- navBarVisible = true;
} else if (bar.equals(BAR_STATUS_BAR)) {
windowInsetsControllerCompat.show(WindowInsetsCompat.Type.statusBars());
} else if (bar.equals(BAR_GESTURE_BAR)) {
diff --git a/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java b/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java
index 0ea955ae88..f72373a88e 100644
--- a/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java
+++ b/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java
@@ -1,7 +1,5 @@
package com.getcapacitor.plugin;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
@@ -15,7 +13,6 @@
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.getcapacitor.Bridge;
-import java.lang.reflect.Field;
import java.lang.reflect.Method;
import org.junit.Test;
import org.mockito.MockedStatic;
@@ -49,66 +46,8 @@ public void showWithNavigationBarShowsOnlyNavigationBars() throws Exception {
verify(controller, never()).show(WindowInsetsCompat.Type.statusBars());
}
- @Test
- public void hideWithEmptyBarHidesSystemBars() throws Exception {
- WindowInsetsControllerCompat controller = invokeSetHidden(new SystemBars(), true, "");
-
- verify(controller).hide(WindowInsetsCompat.Type.systemBars());
- verify(controller, never()).hide(WindowInsetsCompat.Type.statusBars());
- verify(controller, never()).hide(WindowInsetsCompat.Type.navigationBars());
- }
-
- @Test
- public void hideWithStatusBarHidesOnlyStatusBars() throws Exception {
- WindowInsetsControllerCompat controller = invokeSetHidden(new SystemBars(), true, "StatusBar");
-
- verify(controller).hide(WindowInsetsCompat.Type.statusBars());
- verify(controller, never()).hide(WindowInsetsCompat.Type.systemBars());
- verify(controller, never()).hide(WindowInsetsCompat.Type.navigationBars());
- }
-
- @Test
- public void togglingNavigationBarTracksNavBarVisible() throws Exception {
- SystemBars plugin = new SystemBars();
-
- invokeSetHidden(plugin, true, "NavigationBar");
- assertFalse(navBarVisible(plugin));
-
- invokeSetHidden(plugin, false, "NavigationBar");
- assertTrue(navBarVisible(plugin));
- }
-
- @Test
- public void togglingAllBarsTracksNavBarVisible() throws Exception {
- SystemBars plugin = new SystemBars();
-
- invokeSetHidden(plugin, true, "");
- assertFalse(navBarVisible(plugin));
-
- invokeSetHidden(plugin, false, "");
- assertTrue(navBarVisible(plugin));
- }
-
- @Test
- public void hidingOnlyStatusBarLeavesNavBarVisible() throws Exception {
- SystemBars plugin = new SystemBars();
-
- invokeSetHidden(plugin, true, "StatusBar");
-
- assertTrue(navBarVisible(plugin));
- }
-
- private boolean navBarVisible(SystemBars plugin) throws Exception {
- Field field = SystemBars.class.getDeclaredField("navBarVisible");
- field.setAccessible(true);
- return field.getBoolean(plugin);
- }
-
private WindowInsetsControllerCompat invokeSetHidden(String bar) throws Exception {
- return invokeSetHidden(new SystemBars(), false, bar);
- }
-
- private WindowInsetsControllerCompat invokeSetHidden(SystemBars plugin, boolean hide, String bar) throws Exception {
+ SystemBars plugin = new SystemBars();
Bridge bridge = mock(Bridge.class);
AppCompatActivity activity = mock(AppCompatActivity.class);
Window window = mock(Window.class);
@@ -126,7 +65,7 @@ private WindowInsetsControllerCompat invokeSetHidden(SystemBars plugin, boolean
Method setHidden = SystemBars.class.getDeclaredMethod("setHidden", boolean.class, String.class);
setHidden.setAccessible(true);
- setHidden.invoke(plugin, hide, bar);
+ setHidden.invoke(plugin, false, bar);
}
return controller;
diff --git a/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java b/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java
index fd8b686ee6..1006721235 100644
--- a/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java
+++ b/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java
@@ -11,8 +11,8 @@ public class HttpRequestHandlerTest {
static final String BASE_URL = "https://httpbin.org/get";
static final String PARAMS_JSON = """
- {"k": "a&b"}
- """;
+ {"k": "a&b"}
+ """;
@Test
public void testHttpURLConnectionBuilderSetUrlParamsEncoded() throws Exception {
diff --git a/android/package.json b/android/package.json
index f42e65913d..9e1ad80672 100644
--- a/android/package.json
+++ b/android/package.json
@@ -1,9 +1,9 @@
{
- "name": "@capacitor-plus/android",
- "version": "8.5.2",
- "description": "Capacitor+: Enhanced Capacitor with automated upstream sync - Cross-platform apps with JavaScript and the web",
- "homepage": "https://capgo.app/docs/plugins/capacitor-plus/",
- "author": "Capgo Team (https://capgo.app)",
+ "name": "@capacitor/android",
+ "version": "8.5.1",
+ "description": "Capacitor: Cross-platform apps with JavaScript and the web",
+ "homepage": "https://capacitorjs.com",
+ "author": "Ionic Team (https://ionic.io)",
"license": "MIT",
"repository": {
"type": "git",
@@ -23,7 +23,6 @@
"verify": "./gradlew clean lint build test -b capacitor/build.gradle"
},
"peerDependencies": {
- "@capacitor-plus/core": "^8.5.0",
"@capacitor/core": "^8.5.0"
},
"publishConfig": {
diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 64ffed5668..847eac90ee 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -3,29 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-## [8.5.2](https://github.com/Cap-go/capacitor-plus/compare/8.5.1...8.5.2) (2026-08-26)
-
-**Note:** Version bump only for package @capacitor-plus/cli
-
-
-
-
-
-## [8.5.1](https://github.com/Cap-go/capacitor-plus/compare/8.3.12...8.5.1) (2026-08-25)
-
-**Note:** Version bump only for package @capacitor-plus/cli
-
-
+## [8.5.1](https://github.com/ionic-team/capacitor/compare/8.5.0...8.5.1) (2026-08-31)
+### Bug Fixes
+- **cli:** use POSIX paths in CapApp-SPM Package.swift ([#8549](https://github.com/ionic-team/capacitor/issues/8549)) ([5e5bb3b](https://github.com/ionic-team/capacitor/commit/5e5bb3befc312477900252ab07e23b596f8cb0d1))
# [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31)
-## Bug Fixes
+### Bug Fixes
- **cli:** support TypeScript 7 when loading capacitor.config.ts ([#8534](https://github.com/ionic-team/capacitor/issues/8534)) ([4c1c870](https://github.com/ionic-team/capacitor/commit/4c1c8709413b9c19b008c99122ca330cc3c90e6f))
-## Features
+### Features
- **cli:** add migrator functionality for adopting UIScene ([#8544](https://github.com/ionic-team/capacitor/issues/8544)) ([984fa85](https://github.com/ionic-team/capacitor/commit/984fa85ba0adab0aacf895aed6323bf4b503dccb))
@@ -42,11 +32,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02)
-## Bug Fixes
+### Bug Fixes
- **cli:** revert live reload config on failure ([#8485](https://github.com/ionic-team/capacitor/issues/8485)) ([1d031a4](https://github.com/ionic-team/capacitor/commit/1d031a4abec2c793079ba8897ad2e40c4cc6c7f9))
-## Features
+### Features
- **cli:** add experimental packageOptions ([#8471](https://github.com/ionic-team/capacitor/issues/8471)) ([258867b](https://github.com/ionic-team/capacitor/commit/258867b7bf37b1837b99b02ec9638e5a6df08d97))
- **cli:** capture ios_package_manager in telemetry ([#8482](https://github.com/ionic-team/capacitor/issues/8482)) ([b4b297a](https://github.com/ionic-team/capacitor/commit/b4b297a52f8732659662d5e5aaeff81c0f7d9835))
diff --git a/cli/package.json b/cli/package.json
index f7ee82153a..07a14e4f3c 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,9 +1,9 @@
{
- "name": "@capacitor-plus/cli",
- "version": "8.5.2",
- "description": "Capacitor+: Enhanced Capacitor with automated upstream sync - Cross-platform apps with JavaScript and the web",
- "homepage": "https://capgo.app/docs/plugins/capacitor-plus/",
- "author": "Capgo Team (https://capgo.app)",
+ "name": "@capacitor/cli",
+ "version": "8.5.1",
+ "description": "Capacitor: Cross-platform apps with JavaScript and the web",
+ "homepage": "https://capacitorjs.com",
+ "author": "Ionic Team (https://ionic.io)",
"license": "MIT",
"repository": {
"type": "git",
@@ -60,7 +60,6 @@
"semver": "^7.6.3",
"tar": "^7.5.3",
"tslib": "^2.8.1",
- "typescript": "~5.0.2",
"xcode": "^3.0.1",
"xml2js": "^0.6.2"
},
diff --git a/cli/src/ios/update.ts b/cli/src/ios/update.ts
index 8d28888330..8790c9ca35 100644
--- a/cli/src/ios/update.ts
+++ b/cli/src/ios/update.ts
@@ -1,6 +1,6 @@
import { copy, remove, pathExists, readFile, realpath, writeFile } from 'fs-extra';
import { basename, dirname, join, relative } from 'path';
-import { major, prerelease, valid } from 'semver';
+import { major, prerelease } from 'semver';
import c from '../colors';
import { checkPlatformVersions, getCapacitorPackageVersion, runTask } from '../common';
@@ -59,6 +59,30 @@ async function updatePluginFiles(config: Config, plugins: Plugin[], deployment:
await generateCordovaPackageFiles(cordovaPlugins, config);
const validSPMPackages = await checkPluginsForPackageSwift(config, plugins);
+ await Promise.all(
+ validSPMPackages.map(async (plugin) => {
+ const iosPlatformVersion = await getCapacitorPackageVersion(config, config.ios.name);
+ const packageSwiftPath = join(plugin.rootPath, 'Package.swift');
+ let content = await readFile(packageSwiftPath, { encoding: 'utf-8' });
+ const regex = new RegExp(
+ 'url:\\s*"https://github.com/ionic-team/capacitor-swift-pm\\.git",\\s*from:\\s*"([^"]+)"',
+ );
+ const version = content.match(regex)?.[1];
+ const majorCapVersion = major(iosPlatformVersion);
+ if (version && major(version) != majorCapVersion) {
+ const preCapVersion = prerelease(iosPlatformVersion);
+ const forceVersion = preCapVersion ? iosPlatformVersion : `${majorCapVersion}.0.0`;
+ content = setAllStringIn(
+ content,
+ `url: "https://github.com/ionic-team/capacitor-swift-pm.git",`,
+ `)`,
+ ` from: "${forceVersion}"`,
+ );
+ await writeFile(packageSwiftPath, content);
+ logger.warn(`${plugin.id} is built for Capacitor ${major(version)}, it might cause issues`);
+ }
+ }),
+ );
await generatePackageFile(config, validSPMPackages.concat(cordovaPlugins));
diff --git a/cli/src/tasks/migrate-uiscene.ts b/cli/src/tasks/migrate-uiscene.ts
index 40b0bc4200..9f6dfa2789 100644
--- a/cli/src/tasks/migrate-uiscene.ts
+++ b/cli/src/tasks/migrate-uiscene.ts
@@ -134,125 +134,21 @@ async function scanAndWarn(config: Config): Promise {
}
}
-function findMatchingBrace(source: string, openIdx: number): number | null {
+function hasCustomDelegateBody(source: string, sigRegex: RegExp): boolean {
+ const match = source.match(sigRegex);
+ if (!match || match.index === undefined) return false;
+ const openIdx = source.indexOf('{', match.index);
+ if (openIdx === -1) return false;
let depth = 1;
let i = openIdx + 1;
- let inLineComment = false;
- let blockCommentDepth = 0;
- let inString: '"' | '"""' | null = null;
- let stringHashes = 0;
-
while (i < source.length && depth > 0) {
const ch = source[i];
- const next = source[i + 1];
-
- if (inLineComment) {
- if (ch === '\n') inLineComment = false;
- i++;
- continue;
- }
-
- if (blockCommentDepth > 0) {
- if (ch === '*' && next === '/') {
- blockCommentDepth--;
- i += 2;
- continue;
- }
- if (ch === '/' && next === '*') {
- blockCommentDepth++;
- i += 2;
- continue;
- }
- i++;
- continue;
- }
-
- if (inString === '"') {
- if (stringHashes === 0 && ch === '\\') {
- i += 2;
- continue;
- }
- if (ch === '"') {
- let closingHashes = 0;
- while (source[i + 1 + closingHashes] === '#') {
- closingHashes++;
- }
- if (closingHashes === stringHashes) {
- i += 1 + closingHashes;
- inString = null;
- stringHashes = 0;
- continue;
- }
- }
- i++;
- continue;
- }
-
- if (inString === '"""') {
- if (ch === '"' && source[i + 1] === '"' && source[i + 2] === '"') {
- let closingHashes = 0;
- while (source[i + 3 + closingHashes] === '#') {
- closingHashes++;
- }
- if (closingHashes === stringHashes) {
- i += 3 + closingHashes;
- inString = null;
- stringHashes = 0;
- continue;
- }
- }
- i++;
- continue;
- }
-
- if (ch === '/' && next === '/') {
- inLineComment = true;
- i += 2;
- continue;
- }
-
- if (ch === '/' && next === '*') {
- blockCommentDepth++;
- i += 2;
- continue;
- }
-
- if (ch === '#' || ch === '"') {
- let hashes = 0;
- while (source[i + hashes] === '#') {
- hashes++;
- }
- const quoteIdx = i + hashes;
- if (source[quoteIdx] === '"') {
- if (source[quoteIdx + 1] === '"' && source[quoteIdx + 2] === '"') {
- inString = '"""';
- stringHashes = hashes;
- i = quoteIdx + 3;
- continue;
- }
- inString = '"';
- stringHashes = hashes;
- i = quoteIdx + 1;
- continue;
- }
- }
-
if (ch === '{') depth++;
else if (ch === '}') depth--;
i++;
}
-
- return depth === 0 ? i - 1 : null;
-}
-
-function hasCustomDelegateBody(source: string, sigRegex: RegExp): boolean {
- const match = source.match(sigRegex);
- if (!match || match.index === undefined) return false;
- const openIdx = source.indexOf('{', match.index);
- if (openIdx === -1) return false;
- const closeIdx = findMatchingBrace(source, openIdx);
- if (closeIdx === null) return false;
- const body = source.slice(openIdx + 1, closeIdx);
+ if (depth !== 0) return false;
+ const body = source.slice(openIdx + 1, i - 1);
const codeLines = body
.split('\n')
.map((l) => l.trim())
@@ -329,11 +225,18 @@ function extractConfigurationForConnecting(appDelegateSource: string): string |
if (openIdx === -1) {
return null;
}
- const closeIdx = findMatchingBrace(appDelegateSource, openIdx);
- if (closeIdx === null) {
+ let depth = 1;
+ let i = openIdx + 1;
+ while (i < appDelegateSource.length && depth > 0) {
+ const ch = appDelegateSource[i];
+ if (ch === '{') depth++;
+ else if (ch === '}') depth--;
+ i++;
+ }
+ if (depth !== 0) {
return null;
}
- return '\n' + appDelegateSource.slice(sigMatch.index, closeIdx + 1) + '\n';
+ return '\n' + appDelegateSource.slice(sigMatch.index, i) + '\n';
}
function insertBeforeAppDelegateClassEnd(source: string, snippet: string): string | null {
@@ -343,10 +246,18 @@ function insertBeforeAppDelegateClassEnd(source: string, snippet: string): strin
return null;
}
const openIdx = source.indexOf('{', match.index);
- const closeIdx = findMatchingBrace(source, openIdx);
- if (closeIdx === null) {
+ let depth = 1;
+ let i = openIdx + 1;
+ while (i < source.length && depth > 0) {
+ const ch = source[i];
+ if (ch === '{') depth++;
+ else if (ch === '}') depth--;
+ i++;
+ }
+ if (depth !== 0) {
return null;
}
+ const closeIdx = i - 1;
return source.slice(0, closeIdx) + snippet + source.slice(closeIdx);
}
@@ -387,7 +298,6 @@ function describeSignals({
export const __testables = {
classify,
describeSignals,
- findMatchingBrace,
insertBeforeAppDelegateClassEnd,
extractConfigurationForConnecting,
hasCustomDelegateBody,
diff --git a/cli/src/tasks/migrate.ts b/cli/src/tasks/migrate.ts
index ca6f1e7f0c..8631897294 100644
--- a/cli/src/tasks/migrate.ts
+++ b/cli/src/tasks/migrate.ts
@@ -446,7 +446,7 @@ async function writeBreakingChanges() {
)}.`,
);
}
- if (allDependencies['@capacitor/ios'] || allDependencies['@capacitor-plus/ios']) {
+ if (allDependencies['@capacitor/ios']) {
logger.info(
'IMPORTANT: Capacitor 8.5 adopts UIScene on iOS. ' +
'See https://capacitorjs.com/docs/updating/8-5 for the full 8.4 → 8.5 migration guide.',
diff --git a/cli/src/tasks/run.ts b/cli/src/tasks/run.ts
index 780c8130e0..2fef2989f7 100644
--- a/cli/src/tasks/run.ts
+++ b/cli/src/tasks/run.ts
@@ -121,9 +121,6 @@ export async function runCommand(
} catch (e: any) {
if (options.liveReload) {
await CapLiveReloadHelper.revertCapConfigForLiveReload();
- if (liveReloadManifestUpdated && platformName === config.android.name && cordovaPlugins) {
- await writeCordovaAndroidManifest(cordovaPlugins, config, platformName, false);
- }
}
if (!isFatal(e)) {
fatal(e.stack ?? e);
diff --git a/cli/src/util/node.ts b/cli/src/util/node.ts
index 5600b5cad5..2967816113 100644
--- a/cli/src/util/node.ts
+++ b/cli/src/util/node.ts
@@ -30,7 +30,27 @@ const dynamicImport: (specifier: string) => Promise = new Function('specifi
/**
* @see https://github.com/ionic-team/stencil/blob/HEAD/src/compiler/sys/node-require.ts
*/
-function loadWithClassicCompiler(ts: typeof typescript, id: string): unknown {
+export const requireTS = async (ts: typeof typescript, p: string): Promise => {
+ const id = resolve(p);
+
+ if (!hasClassicCompilerAPI(ts)) {
+ // Node has its own built-in TypeScript syntax stripping (stable since Node 23.6, and
+ // available behind --experimental-strip-types since Node 22.6), so we can load the file
+ // directly via the native ESM loader instead of transpiling it ourselves.
+ try {
+ return await dynamicImport(pathToFileURL(id).href);
+ } catch (e: any) {
+ if (e?.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
+ throw new Error(
+ `Your installed version of TypeScript (${ts.version}) no longer provides the compiler API Capacitor previously used to load .ts config files, ` +
+ `and your Node.js runtime (${process.version}) doesn't support loading them natively either.\n` +
+ 'Upgrade to Node.js 22.6+ (running with --experimental-strip-types), or Node.js 23.6+, to continue using capacitor.config.ts.',
+ );
+ }
+ throw e;
+ }
+ }
+
delete require.cache[id];
require.extensions['.ts'] = (module: NodeModuleWithCompile, fileName: string) => {
diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts
index 8c0ee028cd..9f623b7e0f 100644
--- a/cli/src/util/spm.ts
+++ b/cli/src/util/spm.ts
@@ -137,9 +137,9 @@ let package = Package(
const options = packageOptions[plugin.id];
const symlink = options?.symlink;
const symlinkFolder = join('symlinks', plugin.name);
- const relPath = convertToUnixPath(
- symlink ? symlinkFolder : relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath),
- );
+ const relPath = symlink
+ ? symlinkFolder
+ : convertToUnixPath(relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath));
if (symlink) {
await ensureSymlink(plugin.rootPath, resolve(config.ios.nativeProjectDirAbs, 'CapApp-SPM', symlinkFolder));
}
@@ -275,6 +275,7 @@ export async function addSceneManifestIfNeeded(config: Config): Promise {
{
UISceneConfigurationName: 'Default Configuration',
UISceneDelegateClassName: '$(PRODUCT_MODULE_NAME).SceneDelegate',
+ UISceneStoryboardFile: 'Main',
},
],
},
diff --git a/cli/src/util/xcode.ts b/cli/src/util/xcode.ts
index 86a9707e9e..64ab836a34 100644
--- a/cli/src/util/xcode.ts
+++ b/cli/src/util/xcode.ts
@@ -1,5 +1,4 @@
import { writeFileSync } from 'fs-extra';
-import { basename } from 'path';
import { project as loadXcodeProject } from 'xcode';
import type { XcodeProject } from 'xcode';
@@ -21,8 +20,7 @@ export function addSwiftFileToAppTarget(
const project = loadXcodeProject(pbxprojPath);
project.parseSync();
- const targetUuid = project.getFirstTarget().uuid;
- if (project.hasFile(fileRelPath) && isSwiftFileInTargetSources(project, fileRelPath, targetUuid)) {
+ if (project.hasFile(fileRelPath)) {
return { added: false };
}
@@ -31,31 +29,7 @@ export function addSwiftFileToAppTarget(
throw new Error(`Could not find PBXGroup with comment "${groupName}" in ${pbxprojPath}`);
}
- if (project.hasFile(fileRelPath) && !isSwiftFileInTargetSources(project, fileRelPath, targetUuid)) {
- const fileRefUuid = findFileRefUuid(project, fileRelPath);
- if (!fileRefUuid) {
- throw new Error(`Could not find PBXFileReference for ${fileRelPath} in ${pbxprojPath}`);
- }
-
- const group = project.getPBXGroupByKey(groupUuid);
- if (group && !group.children.some((child: { comment?: string }) => child.comment === fileRelPath)) {
- group.children.push({ value: fileRefUuid, comment: fileRelPath });
- }
-
- const file = {
- fileRef: fileRefUuid,
- path: fileRelPath,
- basename: basename(fileRelPath),
- group: 'Sources',
- target: targetUuid,
- uuid: project.generateUuid(),
- };
- (project as XcodeProject & { addToPbxBuildFileSection: (file: unknown) => void }).addToPbxBuildFileSection(file);
- (project as XcodeProject & { addToPbxSourcesBuildPhase: (file: unknown) => void }).addToPbxSourcesBuildPhase(file);
- writeFileSync(pbxprojPath, project.writeSync(), 'utf-8');
- return { added: true };
- }
-
+ const targetUuid = project.getFirstTarget().uuid;
const result = project.addSourceFile(fileRelPath, { target: targetUuid }, groupUuid);
if (!result) {
throw new Error(`Failed to register ${fileRelPath} in ${pbxprojPath}`);
@@ -65,38 +39,6 @@ export function addSwiftFileToAppTarget(
return { added: true };
}
-function fileReferenceMatchesPath(ref: { path?: string }, fileRelPath: string): boolean {
- const path = ref.path;
- return path === fileRelPath || path === `"${fileRelPath}"`;
-}
-
-function findFileRefUuid(project: XcodeProject, fileRelPath: string): string | null {
- const objects = project.hash.project.objects;
- const fileRefs = Object.entries(objects.PBXFileReference).filter(([k]) => !k.endsWith('_comment'));
- const fileRefEntry = fileRefs.find(
- ([, ref]) => typeof ref === 'object' && fileReferenceMatchesPath(ref as { path?: string }, fileRelPath),
- );
- return fileRefEntry?.[0] ?? null;
-}
-
-function isSwiftFileInTargetSources(project: XcodeProject, fileRelPath: string, targetUuid: string): boolean {
- const fileRefUuid = findFileRefUuid(project, fileRelPath);
- if (!fileRefUuid) {
- return false;
- }
-
- const objects = project.hash.project.objects;
- const sourcesPhase = project.pbxSourcesBuildPhaseObj(targetUuid);
- if (!sourcesPhase?.files) {
- return false;
- }
-
- return sourcesPhase.files.some((file: { value?: string }) => {
- const buildFile = objects.PBXBuildFile[file.value ?? ''];
- return typeof buildFile === 'object' && buildFile?.fileRef === fileRefUuid;
- });
-}
-
// Exported for tests.
export function findGroupUuidByComment(project: XcodeProject, comment: string): string | null {
const groups = project.hash.project.objects.PBXGroup;
diff --git a/cli/test/migrate-uiscene-plist.spec.ts b/cli/test/migrate-uiscene-plist.spec.ts
index f07dded8c0..dc3bd25135 100644
--- a/cli/test/migrate-uiscene-plist.spec.ts
+++ b/cli/test/migrate-uiscene-plist.spec.ts
@@ -81,7 +81,7 @@ describe('addSceneManifestIfNeeded', () => {
expect(roleArray).toHaveLength(1);
expect(roleArray[0]['UISceneConfigurationName']).toBe('Default Configuration');
expect(roleArray[0]['UISceneDelegateClassName']).toBe('$(PRODUCT_MODULE_NAME).SceneDelegate');
- expect(roleArray[0]['UISceneStoryboardFile']).toBeUndefined();
+ expect(roleArray[0]['UISceneStoryboardFile']).toBe('Main');
});
it('preserves sibling keys', async () => {
diff --git a/cli/test/migrate-uiscene-scan.spec.ts b/cli/test/migrate-uiscene-scan.spec.ts
index 7fa3f84e34..c0d7964ddf 100644
--- a/cli/test/migrate-uiscene-scan.spec.ts
+++ b/cli/test/migrate-uiscene-scan.spec.ts
@@ -7,7 +7,7 @@ import { __testables } from '../src/tasks/migrate-uiscene';
import { mktmp } from './util';
-const { hasCustomDelegateBody, scanAndWarn, findMatchingBrace } = __testables;
+const { hasCustomDelegateBody, scanAndWarn } = __testables;
const OPEN_URL_SIG = /func application\([^)]*\bopen url:/;
const CONTINUE_SIG = /func application\([^)]*\bcontinue userActivity:/;
@@ -82,26 +82,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
});
});
-describe('findMatchingBrace', () => {
- it('ignores braces inside ordinary string literals', () => {
- const source = 'class AppDelegate {\n let payload = "{\\"a\\":1}"\n}';
- const openIdx = source.indexOf('{');
- expect(findMatchingBrace(source, openIdx)).toBe(source.length - 1);
- });
-
- it('ignores braces inside raw string literals', () => {
- const source = 'class AppDelegate {\n let payload = #"}"#\n}';
- const openIdx = source.indexOf('{');
- expect(findMatchingBrace(source, openIdx)).toBe(source.length - 1);
- });
-
- it('ignores braces inside multiline string literals', () => {
- const source = 'class AppDelegate {\n let payload = """\n }\n """\n}';
- const openIdx = source.indexOf('{');
- expect(findMatchingBrace(source, openIdx)).toBe(source.length - 1);
- });
-});
-
describe('scanAndWarn', () => {
let tmpDir: any;
let iosDir: string;
@@ -172,19 +152,13 @@ describe('scanAndWarn', () => {
expect(warnSpy).not.toHaveBeenCalled();
});
- it('skips Pods/, build/, DerivedData/, and .build/ directories', async () => {
+ it('skips Pods/, build/, and DerivedData/ directories', async () => {
const podsDir = join(iosDir, 'App', 'Pods');
const buildDir = join(iosDir, 'App', 'build');
- const derivedDataDir = join(iosDir, 'App', 'DerivedData');
- const dotBuildDir = join(iosDir, 'App', '.build');
await mkdirp(podsDir);
await mkdirp(buildDir);
- await mkdirp(derivedDataDir);
- await mkdirp(dotBuildDir);
writeFileSync(join(podsDir, 'ThirdParty.swift'), `let x = UIApplication.shared.applicationState\n`);
writeFileSync(join(buildDir, 'Generated.swift'), `class TmpViewController {}\n`);
- writeFileSync(join(derivedDataDir, 'BuildOutput.swift'), `class TmpViewController {}\n`);
- writeFileSync(join(dotBuildDir, 'Artifact.swift'), `class TmpViewController {}\n`);
await scanAndWarn(makeConfig());
diff --git a/cli/test/xcode.spec.ts b/cli/test/xcode.spec.ts
index 6071945ab4..e6aa083570 100644
--- a/cli/test/xcode.spec.ts
+++ b/cli/test/xcode.spec.ts
@@ -23,11 +23,8 @@ describe('findGroupUuidByComment', () => {
const uuid = findGroupUuidByComment(project, 'App');
- if (!uuid) {
- throw new Error('Expected App group UUID');
- }
expect(uuid).toMatch(/^[A-F0-9]{24}$/);
- const group = project.getPBXGroupByKey(uuid);
+ const group = project.getPBXGroupByKey(uuid!);
expect(group).toBeDefined();
expect(group?.path).toBe('App');
});
@@ -41,7 +38,7 @@ describe('findGroupUuidByComment', () => {
});
describe('addSwiftFileToAppTarget', () => {
- let tmpDir: Awaited> | undefined;
+ let tmpDir: any;
let pbxprojPath: string;
beforeEach(async () => {
@@ -50,8 +47,7 @@ describe('addSwiftFileToAppTarget', () => {
});
afterEach(() => {
- const cleanup = tmpDir?.cleanupCallback as unknown as (() => void) | undefined;
- cleanup?.();
+ tmpDir.cleanupCallback();
});
it('registers a new Swift file in all four pbxproj sections', () => {
@@ -78,14 +74,11 @@ describe('addSwiftFileToAppTarget', () => {
buildFiles.some(([k]) => (objects.PBXBuildFile as any)[`${k}_comment`]?.includes('SceneDelegate.swift')),
).toBe(true);
- const appGroupUuid = findGroupUuidByComment(project, 'App');
- if (!appGroupUuid) {
- throw new Error('Expected App group UUID');
- }
- const appGroup = project.getPBXGroupByKey(appGroupUuid);
- expect(appGroup?.children.some((c: any) => c.comment === 'SceneDelegate.swift')).toBe(true);
+ const appGroupUuid = findGroupUuidByComment(project, 'App')!;
+ const appGroup = project.getPBXGroupByKey(appGroupUuid)!;
+ expect(appGroup.children.some((c: any) => c.comment === 'SceneDelegate.swift')).toBe(true);
- const sourcesPhase = objects.PBXSourcesBuildPhase ?? {};
+ const sourcesPhase = objects.PBXSourcesBuildPhase!;
const sourcesEntries = Object.entries(sourcesPhase).filter(([k]) => !k.endsWith('_comment'));
const [, sourcesObj] = sourcesEntries[0];
expect((sourcesObj as any).files.some((f: any) => f.comment?.includes('SceneDelegate.swift'))).toBe(true);
diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md
index 067d0aa2ee..5d03916378 100644
--- a/core/CHANGELOG.md
+++ b/core/CHANGELOG.md
@@ -3,21 +3,11 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-## [8.5.2](https://github.com/Cap-go/capacitor-plus/compare/8.5.1...8.5.2) (2026-08-26)
-
-**Note:** Version bump only for package @capacitor-plus/core
-
-
-
-
-
-## [8.5.1](https://github.com/Cap-go/capacitor-plus/compare/8.3.12...8.5.1) (2026-08-25)
-
-**Note:** Version bump only for package @capacitor-plus/core
-
-
+## [8.5.1](https://github.com/ionic-team/capacitor/compare/8.5.0...8.5.1) (2026-08-31)
+### Bug Fixes
+- **core:** prevent removeListener from removing wrong listener ([#8271](https://github.com/ionic-team/capacitor/issues/8271)) ([5ac4dd6](https://github.com/ionic-team/capacitor/commit/5ac4dd613ae989d8dc8738ea25b77efbd4fa21fe))
# [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31)
diff --git a/core/package.json b/core/package.json
index 48d3934ba6..019c5f9a9e 100644
--- a/core/package.json
+++ b/core/package.json
@@ -1,9 +1,9 @@
{
- "name": "@capacitor-plus/core",
- "version": "8.5.2",
- "description": "Capacitor+: Enhanced Capacitor with automated upstream sync - Cross-platform apps with JavaScript and the web",
- "homepage": "https://capgo.app/docs/plugins/capacitor-plus/",
- "author": "Capgo Team (https://capgo.app)",
+ "name": "@capacitor/core",
+ "version": "8.5.1",
+ "description": "Capacitor: Cross-platform apps with JavaScript and the web",
+ "homepage": "https://capacitorjs.com",
+ "author": "Ionic Team (https://ionic.io)",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/ios-pods-template/App/App/Info.plist b/ios-pods-template/App/App/Info.plist
index 485f40e314..f8c8a57659 100644
--- a/ios-pods-template/App/App/Info.plist
+++ b/ios-pods-template/App/App/Info.plist
@@ -25,19 +25,21 @@
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
-
- UISceneConfigurations
-
- UIWindowSceneSessionRoleApplication
-
-
- UISceneConfigurationName
- Default Configuration
- UISceneDelegateClassName
- $(PRODUCT_MODULE_NAME).SceneDelegate
-
-
-
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneConfigurationName
+ Default Configuration
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
UILaunchStoryboardName
LaunchScreen
diff --git a/ios-spm-template/App/App/Info.plist b/ios-spm-template/App/App/Info.plist
index bdf2461d53..fc41ce69bc 100644
--- a/ios-spm-template/App/App/Info.plist
+++ b/ios-spm-template/App/App/Info.plist
@@ -27,19 +27,21 @@
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
-
- UISceneConfigurations
-
- UIWindowSceneSessionRoleApplication
-
-
- UISceneConfigurationName
- Default Configuration
- UISceneDelegateClassName
- $(PRODUCT_MODULE_NAME).SceneDelegate
-
-
-
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneConfigurationName
+ Default Configuration
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
UILaunchStoryboardName
LaunchScreen
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md
index 7886796ba7..e8f405421e 100644
--- a/ios/CHANGELOG.md
+++ b/ios/CHANGELOG.md
@@ -3,25 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-## [8.5.2](https://github.com/Cap-go/capacitor-plus/compare/8.5.1...8.5.2) (2026-08-26)
-
-**Note:** Version bump only for package @capacitor-plus/ios
-
-
-
-
-
-## [8.5.1](https://github.com/Cap-go/capacitor-plus/compare/8.3.12...8.5.1) (2026-08-25)
-
-**Note:** Version bump only for package @capacitor-plus/ios
-
-
+## [8.5.1](https://github.com/ionic-team/capacitor/compare/8.5.0...8.5.1) (2026-08-31)
+### Bug Fixes
+- block navigation to the internal HTTP proxy path ([ee586ae](https://github.com/ionic-team/capacitor/commit/ee586ae680887ba99d066616f976db149542d922))
# [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31)
-## Features
+### Features
- **ios:** UIScene Support ([#8536](https://github.com/ionic-team/capacitor/issues/8536)) ([3fa04a3](https://github.com/ionic-team/capacitor/commit/3fa04a357c92af34cd6fccb8124791963804a9dc))
@@ -35,7 +25,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02)
-## Features
+### Features
- add method getDouble to plugin config ([#7638](https://github.com/ionic-team/capacitor/issues/7638)) ([93c72de](https://github.com/ionic-team/capacitor/commit/93c72de40a2ec4c78b33659250cb08340083088e))
diff --git a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj
index 573e90cd9a..1bfc57d944 100644
--- a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj
+++ b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj
@@ -89,6 +89,7 @@
A38C3D7B2848BE6F004B3680 /* CapacitorCookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A38C3D7A2848BE6F004B3680 /* CapacitorCookieManager.swift */; };
A71289E627F380A500DADDF3 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71289E527F380A500DADDF3 /* Router.swift */; };
A71289EB27F380FD00DADDF3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71289EA27F380FD00DADDF3 /* RouterTests.swift */; };
+ AA01F00D0000000000000002 /* HttpInterceptorNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA01F00D0000000000000001 /* HttpInterceptorNavigationTests.swift */; };
A7187FD22BD1CB7D00093C45 /* CAPPluginMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7187FD12BD1CB7D00093C45 /* CAPPluginMethod.swift */; };
A76739792B98E09700795F7B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A76739782B98E09700795F7B /* PrivacyInfo.xcprivacy */; };
A771ADEE2C8B845000AF234D /* DateCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A771ADED2C8B845000AF234D /* DateCodableTests.swift */; };
@@ -248,6 +249,7 @@
A38C3D7A2848BE6F004B3680 /* CapacitorCookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapacitorCookieManager.swift; sourceTree = ""; };
A71289E527F380A500DADDF3 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; };
A71289EA27F380FD00DADDF3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = ""; };
+ AA01F00D0000000000000001 /* HttpInterceptorNavigationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpInterceptorNavigationTests.swift; sourceTree = ""; };
A7187FD12BD1CB7D00093C45 /* CAPPluginMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CAPPluginMethod.swift; sourceTree = ""; };
A76739782B98E09700795F7B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
A771ADED2C8B845000AF234D /* DateCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateCodableTests.swift; sourceTree = ""; };
@@ -350,6 +352,7 @@
62E79C712638B23300414164 /* JSExportTests.swift */,
62959BBD2526510200A3D7F1 /* Info.plist */,
A71289EA27F380FD00DADDF3 /* RouterTests.swift */,
+ AA01F00D0000000000000001 /* HttpInterceptorNavigationTests.swift */,
);
path = CapacitorTests;
sourceTree = "";
@@ -777,6 +780,7 @@
62FABD2325AE60BA007B3814 /* BridgedTypesTests.m in Sources */,
621ECCC3254204B700D3D615 /* BridgedTypesTests.swift in Sources */,
A71289EB27F380FD00DADDF3 /* RouterTests.swift in Sources */,
+ AA01F00D0000000000000002 /* HttpInterceptorNavigationTests.swift in Sources */,
6263686025F6EC0100576C1C /* PluginCallAccessorTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift b/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift
index 6ef6235e92..782efe4cff 100644
--- a/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift
+++ b/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift
@@ -7,7 +7,6 @@
//
import Foundation
-import UIKit
@objc(CAPSceneDelegateProxy)
public class SceneDelegateProxy: NSObject, UISceneDelegate {
@@ -22,12 +21,10 @@ public class SceneDelegateProxy: NSObject, UISceneDelegate {
// missed. Deliver them on the first capacitorViewDidAppear, once plugins are
// registered.
var token: NSObjectProtocol?
- token = NotificationCenter.default.addObserver(forName: .capacitorViewDidAppear, object: nil, queue: .main) { [weak self] _ in
- guard let self, Self.isBridgeReady(for: scene) else { return }
+ token = NotificationCenter.default.addObserver(forName: .capacitorViewDidAppear, object: nil, queue: .main) { _ in
if let token {
NotificationCenter.default.removeObserver(token)
}
- token = nil
if !connectionOptions.urlContexts.isEmpty {
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
}
@@ -37,41 +34,6 @@ public class SceneDelegateProxy: NSObject, UISceneDelegate {
}
}
- private static func isBridgeReady(for scene: UIScene) -> Bool {
- guard let windowScene = scene as? UIWindowScene else {
- return false
- }
-
- for window in windowScene.windows {
- guard let bridge = findBridge(in: window.rootViewController) else {
- continue
- }
- if bridge.isViewLoaded && bridge.view.window != nil {
- return true
- }
- }
-
- return false
- }
-
- private static func findBridge(in viewController: UIViewController?) -> CAPBridgeViewController? {
- if let bridge = viewController as? CAPBridgeViewController {
- return bridge
- }
- if let navigationController = viewController as? UINavigationController {
- return findBridge(in: navigationController.visibleViewController)
- }
- if let tabBarController = viewController as? UITabBarController {
- return findBridge(in: tabBarController.selectedViewController)
- }
- for child in viewController?.children ?? [] {
- if let bridge = findBridge(in: child) {
- return bridge
- }
- }
- return nil
- }
-
public func scene(_ scene: UIScene, openURLContexts URLContexts: Set) {
for context in URLContexts {
lastURL = context.url
diff --git a/ios/Capacitor/Capacitor/CapacitorBridge.swift b/ios/Capacitor/Capacitor/CapacitorBridge.swift
index 4251102e4b..657e983357 100644
--- a/ios/Capacitor/Capacitor/CapacitorBridge.swift
+++ b/ios/Capacitor/Capacitor/CapacitorBridge.swift
@@ -213,6 +213,7 @@ open class CapacitorBridge: NSObject, CAPBridgeProtocol {
super.init()
self.webViewDelegationHandler.bridge = self
+ self.webViewAssetHandler.setConfiguration(configuration)
exportCoreJS(localUrl: configuration.localURL.absoluteString)
registerPlugins()
@@ -263,13 +264,16 @@ open class CapacitorBridge: NSObject, CAPBridgeProtocol {
exportCordovaJS()
registerCordovaPlugins()
} else {
- observers.append(NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in
- guard self?.viewController?.view.window?.windowScene == nil else { return }
- self?.triggerDocumentJSEvent(eventName: "resume")
+ observers.append(NotificationCenter.default.addObserver(forName: UIScene.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] notification in
+ if let scene = notification.object as? UIWindowScene, scene === self?.viewController?.view.window?.windowScene {
+ self?.triggerDocumentJSEvent(eventName: "resume")
+ }
+
})
- observers.append(NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in
- guard self?.viewController?.view.window?.windowScene == nil else { return }
- self?.triggerDocumentJSEvent(eventName: "pause")
+ observers.append(NotificationCenter.default.addObserver(forName: UIScene.didEnterBackgroundNotification, object: nil, queue: OperationQueue.main) { [weak self] notification in
+ if let scene = notification.object as? UIWindowScene, scene === self?.viewController?.view.window?.windowScene {
+ self?.triggerDocumentJSEvent(eventName: "pause")
+ }
})
observers.append(NotificationCenter.default.addObserver(forName: UIScene.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] notification in
if let scene = notification.object as? UIWindowScene, scene === self?.viewController?.view.window?.windowScene {
diff --git a/ios/Capacitor/Capacitor/WebViewAssetHandler.swift b/ios/Capacitor/Capacitor/WebViewAssetHandler.swift
index acc8effb74..2e5961dc2c 100644
--- a/ios/Capacitor/Capacitor/WebViewAssetHandler.swift
+++ b/ios/Capacitor/Capacitor/WebViewAssetHandler.swift
@@ -6,6 +6,7 @@ import UniformTypeIdentifiers
open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
private var router: Router
private var serverUrl: URL?
+ private var configuration: InstanceConfiguration?
public init(router: Router) {
self.router = router
@@ -20,6 +21,10 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
self.serverUrl = serverUrl
}
+ open func setConfiguration(_ configuration: InstanceConfiguration?) {
+ self.configuration = configuration
+ }
+
private func isUsingLiveReload(_ localUrl: URL) -> Bool {
return self.serverUrl != nil && self.serverUrl?.scheme != localUrl.scheme
}
@@ -31,7 +36,13 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
let localUrl = URL.init(string: url.absoluteString)!
if url.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) {
- handleCapacitorHttpRequest(urlSchemeTask, localUrl, false)
+ // Only serve the proxy when CapacitorHttp is on. A scheme task can't tell a document from
+ // a subresource, so keeping documents out relies on the check in decidePolicyFor.
+ if configuration?.getPluginConfig("CapacitorHttp").getBoolean("enabled", false) == true {
+ handleCapacitorHttpRequest(urlSchemeTask, localUrl, false)
+ } else {
+ urlSchemeTask.didFailWithError(URLError(.unsupportedURL))
+ }
return
}
@@ -152,12 +163,13 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
let existingHeaders = response.allHeaderFields
var newHeaders: [AnyHashable: Any] = [:]
+ // Nothing should render this. If anything does, sandbox keeps it inert.
+ newHeaders["Content-Security-Policy"] = "sandbox; frame-ancestors 'none'"
+
// if using live reload, then set CORS headers
if self.isUsingLiveReload(url) {
- newHeaders = [
- "Access-Control-Allow-Origin": self.serverUrl?.absoluteString ?? "",
- "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS, TRACE"
- ]
+ newHeaders["Access-Control-Allow-Origin"] = self.serverUrl?.absoluteString ?? ""
+ newHeaders["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS, TRACE"
}
if let mergedHeaders = existingHeaders.merging(newHeaders, uniquingKeysWith: { (_, newHeaders) in newHeaders }) as? [String: String] {
diff --git a/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift b/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift
index 866c7e8c5d..935a4c4117 100644
--- a/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift
+++ b/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift
@@ -74,6 +74,12 @@ open class WebViewDelegationHandler: NSObject, WKNavigationDelegate, WKUIDelegat
return
}
+ // The proxy returns a remote body at the app origin, so block it before plugins can allow it.
+ if navURL.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) {
+ decisionHandler(.cancel)
+ return
+ }
+
// first, give plugins the chance to handle the decision
for pluginObject in bridge.plugins {
let plugin = pluginObject.value
diff --git a/ios/Capacitor/CapacitorTests/HttpInterceptorNavigationTests.swift b/ios/Capacitor/CapacitorTests/HttpInterceptorNavigationTests.swift
new file mode 100644
index 0000000000..eeb2f8c4a6
--- /dev/null
+++ b/ios/Capacitor/CapacitorTests/HttpInterceptorNavigationTests.swift
@@ -0,0 +1,63 @@
+import WebKit
+import XCTest
+
+@testable import Capacitor
+
+private class StubFrameInfo: WKFrameInfo {
+ override var isMainFrame: Bool { false }
+}
+
+private class StubNavigationAction: WKNavigationAction {
+ private let stubbedRequest: URLRequest
+ private let stubbedTargetFrame: WKFrameInfo?
+ init(url: String, subframe: Bool = false) {
+ self.stubbedRequest = URLRequest(url: URL(string: url)!)
+ self.stubbedTargetFrame = subframe ? StubFrameInfo() : nil
+ super.init()
+ }
+ override var request: URLRequest { stubbedRequest }
+ override var targetFrame: WKFrameInfo? { stubbedTargetFrame }
+}
+
+// The proxy path shares the app's origin, so the origin guard alone would allow it.
+class HttpInterceptorNavigationTests: XCTestCase {
+ private var bridge: MockBridge!
+ private var handler: WebViewDelegationHandler!
+ private let webView = WKWebView()
+
+ override func setUp() {
+ super.setUp()
+ let descriptor = InstanceDescriptor()
+ handler = WebViewDelegationHandler()
+ bridge = MockBridge(
+ with: InstanceConfiguration(with: descriptor, isDebug: true),
+ delegate: MockBridgeViewController(),
+ cordovaConfiguration: descriptor.cordovaConfiguration,
+ assetHandler: MockAssetHandler(router: CapacitorRouter()),
+ delegationHandler: handler
+ )
+ }
+
+ private func policy(for url: String) -> WKNavigationActionPolicy {
+ var decision: WKNavigationActionPolicy?
+ handler.webView(webView, decidePolicyFor: StubNavigationAction(url: url)) { decision = $0 }
+ return decision!
+ }
+
+ func testBlocksNavigationToInterceptorPath() {
+ let interceptorURL = "capacitor://localhost\(CapacitorBridge.httpInterceptorStartIdentifier)?u=https://example.com/payload.html"
+ XCTAssertEqual(policy(for: interceptorURL), .cancel)
+ }
+
+ // decidePolicyFor also fires for subframes, and a same-origin iframe gets the bridge too.
+ func testBlocksSubframeNavigationToInterceptorPath() {
+ let interceptorURL = "capacitor://localhost\(CapacitorBridge.httpInterceptorStartIdentifier)?u=https://example.com/payload.html"
+ var decision: WKNavigationActionPolicy?
+ handler.webView(webView, decidePolicyFor: StubNavigationAction(url: interceptorURL, subframe: true)) { decision = $0 }
+ XCTAssertEqual(decision, .cancel)
+ }
+
+ func testAllowsInAppNavigation() {
+ XCTAssertEqual(policy(for: "capacitor://localhost/index.html"), .allow)
+ }
+}
diff --git a/ios/package.json b/ios/package.json
index 0925b653eb..df820652c7 100644
--- a/ios/package.json
+++ b/ios/package.json
@@ -1,9 +1,9 @@
{
- "name": "@capacitor-plus/ios",
- "version": "8.5.2",
- "description": "Capacitor+: Enhanced Capacitor with automated upstream sync - Cross-platform apps with JavaScript and the web",
- "homepage": "https://capgo.app/docs/plugins/capacitor-plus/",
- "author": "Capgo Team (https://capgo.app)",
+ "name": "@capacitor/ios",
+ "version": "8.5.1",
+ "description": "Capacitor: Cross-platform apps with JavaScript and the web",
+ "homepage": "https://capacitorjs.com",
+ "author": "Ionic Team (https://ionic.io)",
"license": "MIT",
"repository": {
"type": "git",
@@ -25,7 +25,6 @@
"xc:build:CapacitorCordova": "cd CapacitorCordova && xcodebuild && cd .."
},
"peerDependencies": {
- "@capacitor-plus/core": "^8.5.0",
"@capacitor/core": "^8.5.0"
},
"publishConfig": {
diff --git a/lerna.json b/lerna.json
index 06ff51191c..7ea0a73133 100644
--- a/lerna.json
+++ b/lerna.json
@@ -13,6 +13,6 @@
"tagVersionPrefix": ""
}
},
- "version": "8.5.2",
+ "version": "8.5.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}