From 23ad7f8a4231a3b78abec32bb67bbd0c415b5011 Mon Sep 17 00:00:00 2001 From: "Github Workflow (on behalf of markemer)" Date: Thu, 7 May 2026 16:55:19 +0000 Subject: [PATCH 01/30] Release 8.3.2 --- CHANGELOG.md | 14 ++++++++++++++ android/CHANGELOG.md | 6 ++++++ android/package.json | 2 +- cli/CHANGELOG.md | 13 +++++++++++++ cli/package.json | 2 +- core/CHANGELOG.md | 4 ++++ core/package.json | 2 +- ios/CHANGELOG.md | 4 ++++ ios/package.json | 2 +- lerna.json | 2 +- 10 files changed, 46 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ee704f6..3851a6663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.2](https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2) (2026-05-07) + +### Bug Fixes + +- **cli:** add cSettings support for compiler flags in generated Package.swift ([#8448](https://github.com/ionic-team/capacitor/issues/8448)) ([0bd0676](https://github.com/ionic-team/capacitor/commit/0bd0676315c5fd77e50312dd7b5bf4990dcbd7d0)) +- **cli:** add system framework and weak framework support in SPM Package.swift ([#8447](https://github.com/ionic-team/capacitor/issues/8447)) ([3232f0f](https://github.com/ionic-team/capacitor/commit/3232f0fe1d9811b0b5c500e3dc05cb8a250177f8)) +- **cli:** correct Capacitor plugin SPM compat check ([#8440](https://github.com/ionic-team/capacitor/issues/8440)) ([e5ccc45](https://github.com/ionic-team/capacitor/commit/e5ccc451dda27d56bca824ed644bd20fe4d988cb)) +- **cli:** generate binaryTarget entries for custom xcframeworks in Package.swift ([#8445](https://github.com/ionic-team/capacitor/issues/8445)) ([1f7e33f](https://github.com/ionic-team/capacitor/commit/1f7e33fca43d183332ec19d22b0d75ef81d8cc6d)) +- **cli:** generate resource entries in Package.swift ([#8455](https://github.com/ionic-team/capacitor/issues/8455)) ([790bd27](https://github.com/ionic-team/capacitor/commit/790bd27123497111984227010c3162cec94a108e)) +- **cli:** handle Cordova plugins without iOS source files ([#8443](https://github.com/ionic-team/capacitor/issues/8443)) ([0da130e](https://github.com/ionic-team/capacitor/commit/0da130eb7a861bee4e2c35bc0aac53ba9c983fc3)) +- **cli:** link plugin dependencies in Package.swift ([#8457](https://github.com/ionic-team/capacitor/issues/8457)) ([b3c769e](https://github.com/ionic-team/capacitor/commit/b3c769e856c826b1174518877cf86ac7ce73bf09)) +- **ios:** support Cordova plugins with Package.swift ([#8438](https://github.com/ionic-team/capacitor/issues/8438)) ([139943b](https://github.com/ionic-team/capacitor/commit/139943b0c05fddb2d1ce2d6f468800fddf17b4cf)) +- **SystemBars:** avoid extra view padding on API <= 34 ([#8439](https://github.com/ionic-team/capacitor/issues/8439)) ([5b135a7](https://github.com/ionic-team/capacitor/commit/5b135a70217be560e7176c8d5b514cc92ed3e4e4)) + ## [8.3.1](https://github.com/ionic-team/capacitor/compare/8.3.0...8.3.1) (2026-04-16) ### Bug Fixes diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 5812a4832..ca3b6f0ef 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.2](https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2) (2026-05-07) + +### Bug Fixes + +- **SystemBars:** avoid extra view padding on API <= 34 ([#8439](https://github.com/ionic-team/capacitor/issues/8439)) ([5b135a7](https://github.com/ionic-team/capacitor/commit/5b135a70217be560e7176c8d5b514cc92ed3e4e4)) + ## [8.3.1](https://github.com/ionic-team/capacitor/compare/8.3.0...8.3.1) (2026-04-16) ### Bug Fixes diff --git a/android/package.json b/android/package.json index f55229c31..5ecf01e07 100644 --- a/android/package.json +++ b/android/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/android", - "version": "8.3.1", + "version": "8.3.2", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index ac66f222f..1cc5720a1 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.2](https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2) (2026-05-07) + +### Bug Fixes + +- **cli:** add cSettings support for compiler flags in generated Package.swift ([#8448](https://github.com/ionic-team/capacitor/issues/8448)) ([0bd0676](https://github.com/ionic-team/capacitor/commit/0bd0676315c5fd77e50312dd7b5bf4990dcbd7d0)) +- **cli:** add system framework and weak framework support in SPM Package.swift ([#8447](https://github.com/ionic-team/capacitor/issues/8447)) ([3232f0f](https://github.com/ionic-team/capacitor/commit/3232f0fe1d9811b0b5c500e3dc05cb8a250177f8)) +- **cli:** correct Capacitor plugin SPM compat check ([#8440](https://github.com/ionic-team/capacitor/issues/8440)) ([e5ccc45](https://github.com/ionic-team/capacitor/commit/e5ccc451dda27d56bca824ed644bd20fe4d988cb)) +- **cli:** generate binaryTarget entries for custom xcframeworks in Package.swift ([#8445](https://github.com/ionic-team/capacitor/issues/8445)) ([1f7e33f](https://github.com/ionic-team/capacitor/commit/1f7e33fca43d183332ec19d22b0d75ef81d8cc6d)) +- **cli:** generate resource entries in Package.swift ([#8455](https://github.com/ionic-team/capacitor/issues/8455)) ([790bd27](https://github.com/ionic-team/capacitor/commit/790bd27123497111984227010c3162cec94a108e)) +- **cli:** handle Cordova plugins without iOS source files ([#8443](https://github.com/ionic-team/capacitor/issues/8443)) ([0da130e](https://github.com/ionic-team/capacitor/commit/0da130eb7a861bee4e2c35bc0aac53ba9c983fc3)) +- **cli:** link plugin dependencies in Package.swift ([#8457](https://github.com/ionic-team/capacitor/issues/8457)) ([b3c769e](https://github.com/ionic-team/capacitor/commit/b3c769e856c826b1174518877cf86ac7ce73bf09)) +- **ios:** support Cordova plugins with Package.swift ([#8438](https://github.com/ionic-team/capacitor/issues/8438)) ([139943b](https://github.com/ionic-team/capacitor/commit/139943b0c05fddb2d1ce2d6f468800fddf17b4cf)) + ## [8.3.1](https://github.com/ionic-team/capacitor/compare/8.3.0...8.3.1) (2026-04-16) ### Bug Fixes diff --git a/cli/package.json b/cli/package.json index b9f5d1ccc..abc3774df 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/cli", - "version": "8.3.1", + "version": "8.3.2", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 60a9ca82b..08c8815c3 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.2](https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2) (2026-05-07) + +**Note:** Version bump only for package @capacitor/core + ## [8.3.1](https://github.com/ionic-team/capacitor/compare/8.3.0...8.3.1) (2026-04-16) **Note:** Version bump only for package @capacitor/core diff --git a/core/package.json b/core/package.json index 54e8c7110..bc3bebc0f 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/core", - "version": "8.3.1", + "version": "8.3.2", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index a16975c79..111b642ac 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.2](https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2) (2026-05-07) + +**Note:** Version bump only for package @capacitor/ios + ## [8.3.1](https://github.com/ionic-team/capacitor/compare/8.3.0...8.3.1) (2026-04-16) ### Bug Fixes diff --git a/ios/package.json b/ios/package.json index 4f7200cad..0972ed946 100644 --- a/ios/package.json +++ b/ios/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/ios", - "version": "8.3.1", + "version": "8.3.2", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/lerna.json b/lerna.json index 5f513f656..d0a1ebc02 100644 --- a/lerna.json +++ b/lerna.json @@ -11,6 +11,6 @@ "tagVersionPrefix": "" } }, - "version": "8.3.1", + "version": "8.3.2", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } From b2d771926a180e60deea31992d7d4abcd5ca3bc7 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Fri, 8 May 2026 18:44:22 +0200 Subject: [PATCH 02/30] fix(cli): copy plugin files in CocoaPods projects (#8467) --- cli/src/ios/update.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/ios/update.ts b/cli/src/ios/update.ts index 9fcd1c718..82ca85a88 100644 --- a/cli/src/ios/update.ts +++ b/cli/src/ios/update.ts @@ -564,7 +564,7 @@ async function copyPluginsNativeFiles(config: Config, cordovaPlugins: Plugin[]) const isSPM = (await config.ios.packageManager) === 'SPM'; for (const p of cordovaPlugins) { const platformTag = getPluginPlatform(p, platform); - if (platformTag.$?.package) { + if (isSPM && platformTag.$?.package) { continue; } const sourceFiles = getPlatformElement(p, platform, 'source-file'); From 76ff70e0e40bbbca63c5195c572f166e5ce82089 Mon Sep 17 00:00:00 2001 From: "Github Workflow (on behalf of markemer)" Date: Fri, 8 May 2026 17:08:40 +0000 Subject: [PATCH 03/30] Release 8.3.3 --- CHANGELOG.md | 6 ++++++ android/CHANGELOG.md | 4 ++++ android/package.json | 2 +- cli/CHANGELOG.md | 6 ++++++ cli/package.json | 2 +- core/CHANGELOG.md | 4 ++++ core/package.json | 2 +- ios/CHANGELOG.md | 4 ++++ ios/package.json | 2 +- lerna.json | 2 +- 10 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3851a6663..d4ad41ff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.3](https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3) (2026-05-08) + +### Bug Fixes + +- **cli:** copy plugin files in CocoaPods projects ([#8467](https://github.com/ionic-team/capacitor/issues/8467)) ([b2d7719](https://github.com/ionic-team/capacitor/commit/b2d771926a180e60deea31992d7d4abcd5ca3bc7)) + ## [8.3.2](https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2) (2026-05-07) ### Bug Fixes diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index ca3b6f0ef..d4b2fc534 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.3](https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3) (2026-05-08) + +**Note:** Version bump only for package @capacitor/android + ## [8.3.2](https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2) (2026-05-07) ### Bug Fixes diff --git a/android/package.json b/android/package.json index 5ecf01e07..07db8a26e 100644 --- a/android/package.json +++ b/android/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/android", - "version": "8.3.2", + "version": "8.3.3", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 1cc5720a1..452ba4cc6 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.3](https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3) (2026-05-08) + +### Bug Fixes + +- **cli:** copy plugin files in CocoaPods projects ([#8467](https://github.com/ionic-team/capacitor/issues/8467)) ([b2d7719](https://github.com/ionic-team/capacitor/commit/b2d771926a180e60deea31992d7d4abcd5ca3bc7)) + ## [8.3.2](https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2) (2026-05-07) ### Bug Fixes diff --git a/cli/package.json b/cli/package.json index abc3774df..cd17f5d2f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/cli", - "version": "8.3.2", + "version": "8.3.3", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 08c8815c3..26f5723fe 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.3](https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3) (2026-05-08) + +**Note:** Version bump only for package @capacitor/core + ## [8.3.2](https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2) (2026-05-07) **Note:** Version bump only for package @capacitor/core diff --git a/core/package.json b/core/package.json index bc3bebc0f..373249f98 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/core", - "version": "8.3.2", + "version": "8.3.3", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 111b642ac..b9b7863b0 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.3](https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3) (2026-05-08) + +**Note:** Version bump only for package @capacitor/ios + ## [8.3.2](https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2) (2026-05-07) **Note:** Version bump only for package @capacitor/ios diff --git a/ios/package.json b/ios/package.json index 0972ed946..bd4c0a320 100644 --- a/ios/package.json +++ b/ios/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/ios", - "version": "8.3.2", + "version": "8.3.3", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/lerna.json b/lerna.json index d0a1ebc02..d64bfcd44 100644 --- a/lerna.json +++ b/lerna.json @@ -11,6 +11,6 @@ "tagVersionPrefix": "" } }, - "version": "8.3.2", + "version": "8.3.3", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } From b7e167bbab47c57a5f72ff9e76a726ab7d8a1c1f Mon Sep 17 00:00:00 2001 From: "Alex J." Date: Tue, 12 May 2026 15:19:58 -0400 Subject: [PATCH 04/30] chore(cli): update telemetry endpoint (#8464) --- cli/src/ipc.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/src/ipc.ts b/cli/src/ipc.ts index 41894e53a..37c38110b 100644 --- a/cli/src/ipc.ts +++ b/cli/src/ipc.ts @@ -49,9 +49,9 @@ export async function receive(msg: IPCMessage): Promise { // This request is only made if telemetry is on. const req = request( { - hostname: 'api.ionicjs.com', + hostname: 'metrics-capacitor.outsystems.com', port: 443, - path: '/events/metrics', + path: '/metrics', method: 'POST', headers: { 'Content-Type': 'application/json', @@ -60,7 +60,7 @@ export async function receive(msg: IPCMessage): Promise { (response) => { debug('Sent %O metric to events service (status: %O)', data.name, response.statusCode); - if (response.statusCode !== 204) { + if (response.statusCode !== 202) { response.on('data', (chunk) => { debug('Bad response from events service. Request body: %O', chunk.toString()); }); From de1e506e48204f3d9a51bc911fafc341fd81fc36 Mon Sep 17 00:00:00 2001 From: "Github Workflow (on behalf of alexgerardojacinto)" Date: Tue, 12 May 2026 20:38:58 +0000 Subject: [PATCH 05/30] Release 8.3.4 --- CHANGELOG.md | 4 ++++ android/CHANGELOG.md | 4 ++++ android/package.json | 2 +- cli/CHANGELOG.md | 4 ++++ cli/package.json | 2 +- core/CHANGELOG.md | 4 ++++ core/package.json | 2 +- ios/CHANGELOG.md | 4 ++++ ios/package.json | 2 +- lerna.json | 2 +- 10 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ad41ff2..a7f2b7713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.4](https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4) (2026-05-12) + +**Note:** Version bump only for package capacitor + ## [8.3.3](https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3) (2026-05-08) ### Bug Fixes diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index d4b2fc534..74d538d9f 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.4](https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4) (2026-05-12) + +**Note:** Version bump only for package @capacitor/android + ## [8.3.3](https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3) (2026-05-08) **Note:** Version bump only for package @capacitor/android diff --git a/android/package.json b/android/package.json index 07db8a26e..8ae93f3f2 100644 --- a/android/package.json +++ b/android/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/android", - "version": "8.3.3", + "version": "8.3.4", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 452ba4cc6..bba24688f 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.4](https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4) (2026-05-12) + +**Note:** Version bump only for package @capacitor/cli + ## [8.3.3](https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3) (2026-05-08) ### Bug Fixes diff --git a/cli/package.json b/cli/package.json index cd17f5d2f..9df346b14 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/cli", - "version": "8.3.3", + "version": "8.3.4", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 26f5723fe..017293794 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.4](https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4) (2026-05-12) + +**Note:** Version bump only for package @capacitor/core + ## [8.3.3](https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3) (2026-05-08) **Note:** Version bump only for package @capacitor/core diff --git a/core/package.json b/core/package.json index 373249f98..eef113a50 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/core", - "version": "8.3.3", + "version": "8.3.4", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index b9b7863b0..87ee73bb4 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.3.4](https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4) (2026-05-12) + +**Note:** Version bump only for package @capacitor/ios + ## [8.3.3](https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3) (2026-05-08) **Note:** Version bump only for package @capacitor/ios diff --git a/ios/package.json b/ios/package.json index bd4c0a320..a7b8d3133 100644 --- a/ios/package.json +++ b/ios/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/ios", - "version": "8.3.3", + "version": "8.3.4", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/lerna.json b/lerna.json index d64bfcd44..1e53d6bdc 100644 --- a/lerna.json +++ b/lerna.json @@ -11,6 +11,6 @@ "tagVersionPrefix": "" } }, - "version": "8.3.3", + "version": "8.3.4", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } From 731a82e554d9c1270241897228e934c38759bbbb Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Thu, 21 May 2026 17:39:31 +0200 Subject: [PATCH 06/30] chore: format java code (#8475) --- .../com/getcapacitor/BridgeWebChromeClient.java | 12 ++++++------ .../src/main/java/com/getcapacitor/FileUtils.java | 6 +++--- .../src/main/java/com/getcapacitor/Plugin.java | 4 ++-- .../cordova/MockCordovaWebViewImpl.java | 14 ++++++-------- .../com/getcapacitor/plugin/CapacitorHttp.java | 4 ++-- .../java/com/getcapacitor/ConfigBuildingTest.java | 6 +++--- 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java index 6ca1c8a83..58371a9ff 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java +++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java @@ -246,14 +246,14 @@ public boolean onJsPrompt(WebView view, String url, String message, String defau public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { super.onGeolocationPermissionsShowPrompt(origin, callback); Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: " + origin); - final String[] geoPermissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }; + final String[] geoPermissions = {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION}; if (!PermissionHelper.hasPermissions(bridge.getContext(), geoPermissions)) { permissionListener = (isGranted) -> { if (isGranted) { callback.invoke(origin, true, false); } else { - final String[] coarsePermission = { Manifest.permission.ACCESS_COARSE_LOCATION }; + final String[] coarsePermission = {Manifest.permission.ACCESS_COARSE_LOCATION}; if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && PermissionHelper.hasPermissions(bridge.getContext(), coarsePermission) @@ -294,7 +294,7 @@ public boolean onShowFileChooser( filePathCallback.onReceiveValue(null); } }; - final String[] camPermission = { Manifest.permission.CAMERA }; + final String[] camPermission = {Manifest.permission.CAMERA}; permissionLauncher.launch(camPermission); } } else { @@ -305,7 +305,7 @@ public boolean onShowFileChooser( } private boolean isMediaCaptureSupported() { - String[] permissions = { Manifest.permission.CAMERA }; + String[] permissions = {Manifest.permission.CAMERA}; return ( PermissionHelper.hasPermissions(bridge.getContext(), permissions) || !PermissionHelper.hasDefinedPermission(bridge.getContext(), Manifest.permission.CAMERA) @@ -343,7 +343,7 @@ private boolean showImageCapturePicker(final ValueCallback filePathCallba activityListener = (activityResult) -> { Uri[] result = null; if (activityResult.getResultCode() == Activity.RESULT_OK) { - result = new Uri[] { imageFileUri }; + result = new Uri[] {imageFileUri}; } filePathCallback.onReceiveValue(result); }; @@ -362,7 +362,7 @@ private boolean showVideoCapturePicker(final ValueCallback filePathCallba activityListener = (activityResult) -> { Uri[] result = null; if (activityResult.getResultCode() == Activity.RESULT_OK) { - result = new Uri[] { activityResult.getData().getData() }; + result = new Uri[] {activityResult.getData().getData()}; } filePathCallback.onReceiveValue(result); }; diff --git a/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java b/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java index ce09e7c2f..0fdf64feb 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java +++ b/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java @@ -118,7 +118,7 @@ else if (isMediaDocument(uri)) { } final String selection = "_id=?"; - final String[] selectionArgs = new String[] { split[1] }; + final String[] selectionArgs = new String[] {split[1]}; return getDataColumn(context, contentUri, selection, selectionArgs); } @@ -195,7 +195,7 @@ private static String getDataColumn(Context context, Uri uri, String selection, String path = null; Cursor cursor = null; final String column = "_data"; - final String[] projection = { column }; + final String[] projection = {column}; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); @@ -292,7 +292,7 @@ private static String getPathToNonPrimaryVolume(Context context, String tag) { } private static String sanitizeFilename(String displayName) { - String[] badCharacters = new String[] { "..", "/" }; + String[] badCharacters = new String[] {"..", "/"}; String[] segments = displayName.split("/"); String fileName = segments[segments.length - 1]; for (String suspString : badCharacters) { diff --git a/android/capacitor/src/main/java/com/getcapacitor/Plugin.java b/android/capacitor/src/main/java/com/getcapacitor/Plugin.java index 19c8b8b3a..e55d933cc 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/Plugin.java +++ b/android/capacitor/src/main/java/com/getcapacitor/Plugin.java @@ -465,7 +465,7 @@ protected void requestAllPermissions(@NonNull PluginCall call, @NonNull String c * @param callbackName the name of the callback to run when the permission request is complete */ protected void requestPermissionForAlias(@NonNull String alias, @NonNull PluginCall call, @NonNull String callbackName) { - requestPermissionForAliases(new String[] { alias }, call, callbackName); + requestPermissionForAliases(new String[] {alias}, call, callbackName); } /** @@ -583,7 +583,7 @@ public void pluginRequestAllPermissions() { */ @Deprecated public void pluginRequestPermission(String permission, int requestCode) { - ActivityCompat.requestPermissions(getActivity(), new String[] { permission }, requestCode); + ActivityCompat.requestPermissions(getActivity(), new String[] {permission}, requestCode); } /** 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 1115429d7..020c1457a 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/CapacitorHttp.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java index 46bc17417..7d5ab337e 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java @@ -18,8 +18,8 @@ @CapacitorPlugin( permissions = { - @Permission(strings = { Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "HttpWrite"), - @Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE }, alias = "HttpRead") + @Permission(strings = {Manifest.permission.WRITE_EXTERNAL_STORAGE}, alias = "HttpWrite"), + @Permission(strings = {Manifest.permission.READ_EXTERNAL_STORAGE}, alias = "HttpRead") } ) public class CapacitorHttp extends Plugin { diff --git a/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java b/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java index 1af51039d..ae12edb02 100644 --- a/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java +++ b/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java @@ -42,7 +42,7 @@ public void setup() { config = new CapConfig.Builder(context) .setAllowMixedContent(true) - .setAllowNavigation(new String[] { "http://www.google.com" }) + .setAllowNavigation(new String[] {"http://www.google.com"}) .setAndroidScheme("test") .setCaptureInput(true) .setLoggingEnabled(true) @@ -64,7 +64,7 @@ public void setup() { @Test public void getCoreConfigValues() { assertTrue(config.isMixedContentAllowed()); - assertArrayEquals(new String[] { "http://www.google.com" }, config.getAllowNavigation()); + assertArrayEquals(new String[] {"http://www.google.com"}, config.getAllowNavigation()); assertEquals("test", config.getAndroidScheme()); assertTrue(config.isInputCaptured()); assertTrue(config.isLoggingEnabled()); @@ -97,7 +97,7 @@ public void getPluginInt() { @Test public void getPluginArray() { - String[] comparison = new String[] { "5", "6", "7", "8" }; + String[] comparison = new String[] {"5", "6", "7", "8"}; String[] testArray = config.getPluginConfiguration(TEST_PLUGIN_NAME).getArray("var5"); assertArrayEquals(comparison, testArray); } From bc745364129853769ca2fcfc3cc2a6c7624ee513 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Thu, 21 May 2026 18:04:37 +0200 Subject: [PATCH 07/30] chore: format java code (#8477) --- .../java/com/getcapacitor/BridgeWebChromeClient.java | 12 ++++++------ .../src/main/java/com/getcapacitor/FileUtils.java | 6 +++--- .../src/main/java/com/getcapacitor/Plugin.java | 4 ++-- .../java/com/getcapacitor/plugin/CapacitorHttp.java | 4 ++-- .../java/com/getcapacitor/ConfigBuildingTest.java | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java index 58371a9ff..6ca1c8a83 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java +++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java @@ -246,14 +246,14 @@ public boolean onJsPrompt(WebView view, String url, String message, String defau public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { super.onGeolocationPermissionsShowPrompt(origin, callback); Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: " + origin); - final String[] geoPermissions = {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION}; + final String[] geoPermissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }; if (!PermissionHelper.hasPermissions(bridge.getContext(), geoPermissions)) { permissionListener = (isGranted) -> { if (isGranted) { callback.invoke(origin, true, false); } else { - final String[] coarsePermission = {Manifest.permission.ACCESS_COARSE_LOCATION}; + final String[] coarsePermission = { Manifest.permission.ACCESS_COARSE_LOCATION }; if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && PermissionHelper.hasPermissions(bridge.getContext(), coarsePermission) @@ -294,7 +294,7 @@ public boolean onShowFileChooser( filePathCallback.onReceiveValue(null); } }; - final String[] camPermission = {Manifest.permission.CAMERA}; + final String[] camPermission = { Manifest.permission.CAMERA }; permissionLauncher.launch(camPermission); } } else { @@ -305,7 +305,7 @@ public boolean onShowFileChooser( } private boolean isMediaCaptureSupported() { - String[] permissions = {Manifest.permission.CAMERA}; + String[] permissions = { Manifest.permission.CAMERA }; return ( PermissionHelper.hasPermissions(bridge.getContext(), permissions) || !PermissionHelper.hasDefinedPermission(bridge.getContext(), Manifest.permission.CAMERA) @@ -343,7 +343,7 @@ private boolean showImageCapturePicker(final ValueCallback filePathCallba activityListener = (activityResult) -> { Uri[] result = null; if (activityResult.getResultCode() == Activity.RESULT_OK) { - result = new Uri[] {imageFileUri}; + result = new Uri[] { imageFileUri }; } filePathCallback.onReceiveValue(result); }; @@ -362,7 +362,7 @@ private boolean showVideoCapturePicker(final ValueCallback filePathCallba activityListener = (activityResult) -> { Uri[] result = null; if (activityResult.getResultCode() == Activity.RESULT_OK) { - result = new Uri[] {activityResult.getData().getData()}; + result = new Uri[] { activityResult.getData().getData() }; } filePathCallback.onReceiveValue(result); }; diff --git a/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java b/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java index 0fdf64feb..ce09e7c2f 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java +++ b/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java @@ -118,7 +118,7 @@ else if (isMediaDocument(uri)) { } final String selection = "_id=?"; - final String[] selectionArgs = new String[] {split[1]}; + final String[] selectionArgs = new String[] { split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); } @@ -195,7 +195,7 @@ private static String getDataColumn(Context context, Uri uri, String selection, String path = null; Cursor cursor = null; final String column = "_data"; - final String[] projection = {column}; + final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); @@ -292,7 +292,7 @@ private static String getPathToNonPrimaryVolume(Context context, String tag) { } private static String sanitizeFilename(String displayName) { - String[] badCharacters = new String[] {"..", "/"}; + String[] badCharacters = new String[] { "..", "/" }; String[] segments = displayName.split("/"); String fileName = segments[segments.length - 1]; for (String suspString : badCharacters) { diff --git a/android/capacitor/src/main/java/com/getcapacitor/Plugin.java b/android/capacitor/src/main/java/com/getcapacitor/Plugin.java index e55d933cc..19c8b8b3a 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/Plugin.java +++ b/android/capacitor/src/main/java/com/getcapacitor/Plugin.java @@ -465,7 +465,7 @@ protected void requestAllPermissions(@NonNull PluginCall call, @NonNull String c * @param callbackName the name of the callback to run when the permission request is complete */ protected void requestPermissionForAlias(@NonNull String alias, @NonNull PluginCall call, @NonNull String callbackName) { - requestPermissionForAliases(new String[] {alias}, call, callbackName); + requestPermissionForAliases(new String[] { alias }, call, callbackName); } /** @@ -583,7 +583,7 @@ public void pluginRequestAllPermissions() { */ @Deprecated public void pluginRequestPermission(String permission, int requestCode) { - ActivityCompat.requestPermissions(getActivity(), new String[] {permission}, requestCode); + ActivityCompat.requestPermissions(getActivity(), new String[] { permission }, requestCode); } /** diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java index 7d5ab337e..46bc17417 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java @@ -18,8 +18,8 @@ @CapacitorPlugin( permissions = { - @Permission(strings = {Manifest.permission.WRITE_EXTERNAL_STORAGE}, alias = "HttpWrite"), - @Permission(strings = {Manifest.permission.READ_EXTERNAL_STORAGE}, alias = "HttpRead") + @Permission(strings = { Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "HttpWrite"), + @Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE }, alias = "HttpRead") } ) public class CapacitorHttp extends Plugin { diff --git a/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java b/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java index ae12edb02..1af51039d 100644 --- a/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java +++ b/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java @@ -42,7 +42,7 @@ public void setup() { config = new CapConfig.Builder(context) .setAllowMixedContent(true) - .setAllowNavigation(new String[] {"http://www.google.com"}) + .setAllowNavigation(new String[] { "http://www.google.com" }) .setAndroidScheme("test") .setCaptureInput(true) .setLoggingEnabled(true) @@ -64,7 +64,7 @@ public void setup() { @Test public void getCoreConfigValues() { assertTrue(config.isMixedContentAllowed()); - assertArrayEquals(new String[] {"http://www.google.com"}, config.getAllowNavigation()); + assertArrayEquals(new String[] { "http://www.google.com" }, config.getAllowNavigation()); assertEquals("test", config.getAndroidScheme()); assertTrue(config.isInputCaptured()); assertTrue(config.isLoggingEnabled()); @@ -97,7 +97,7 @@ public void getPluginInt() { @Test public void getPluginArray() { - String[] comparison = new String[] {"5", "6", "7", "8"}; + String[] comparison = new String[] { "5", "6", "7", "8" }; String[] testArray = config.getPluginConfiguration(TEST_PLUGIN_NAME).getArray("var5"); assertArrayEquals(comparison, testArray); } From 93c72de40a2ec4c78b33659250cb08340083088e Mon Sep 17 00:00:00 2001 From: Robin Genz Date: Thu, 21 May 2026 18:54:21 +0200 Subject: [PATCH 08/30] feat: add method getDouble to plugin config (#7638) Co-authored-by: jcesarmobile --- .../java/com/getcapacitor/PluginConfig.java | 11 ++++++++++ .../java/com/getcapacitor/util/JSONUtils.java | 20 +++++++++++++++++++ ios/Capacitor/Capacitor/PluginConfig.swift | 7 +++++++ 3 files changed, 38 insertions(+) diff --git a/android/capacitor/src/main/java/com/getcapacitor/PluginConfig.java b/android/capacitor/src/main/java/com/getcapacitor/PluginConfig.java index 0f00fc530..1d21ae90e 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/PluginConfig.java +++ b/android/capacitor/src/main/java/com/getcapacitor/PluginConfig.java @@ -65,6 +65,17 @@ public int getInt(String configKey, int defaultValue) { return JSONUtils.getInt(config, configKey, defaultValue); } + /** + * Get a double value for a plugin in the Capacitor config. + * + * @param configKey The key of the value to retrieve + * @param defaultValue A default value to return if the key does not exist in the config + * @return The value from the config, if key exists. Default value returned if not + */ + public double getDouble(String configKey, double defaultValue) { + return JSONUtils.getDouble(config, configKey, defaultValue); + } + /** * Get a string array value for a plugin in the Capacitor config. * diff --git a/android/capacitor/src/main/java/com/getcapacitor/util/JSONUtils.java b/android/capacitor/src/main/java/com/getcapacitor/util/JSONUtils.java index 1d2fc2078..f24e5aa0b 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/util/JSONUtils.java +++ b/android/capacitor/src/main/java/com/getcapacitor/util/JSONUtils.java @@ -75,6 +75,26 @@ public static int getInt(JSONObject jsonObject, String key, int defaultValue) { return defaultValue; } + /** + * Get a double value from the given JSON object. + * + * @param jsonObject A JSON object to search + * @param key A key to fetch from the JSON object + * @param defaultValue A default value to return if the key cannot be found + * @return The value at the given key in the JSON object, or the default value + */ + public static double getDouble(JSONObject jsonObject, String key, double defaultValue) { + String k = getDeepestKey(key); + try { + JSONObject o = getDeepestObject(jsonObject, key); + return o.getDouble(k); + } catch (JSONException ignore) { + // value was not found + } + + return defaultValue; + } + /** * Get a JSON object value from the given JSON object. * diff --git a/ios/Capacitor/Capacitor/PluginConfig.swift b/ios/Capacitor/Capacitor/PluginConfig.swift index f2461e102..8d73bc1de 100644 --- a/ios/Capacitor/Capacitor/PluginConfig.swift +++ b/ios/Capacitor/Capacitor/PluginConfig.swift @@ -30,6 +30,13 @@ import Foundation return defaultValue } + @objc public func getDouble(_ configKey: String, _ defaultValue: Double) -> Double { + if let val = (self.config)[keyPath: KeyPath(configKey)] as? Double { + return val + } + return defaultValue + } + public func getArray(_ configKey: String, _ defaultValue: JSArray? = nil) -> JSArray? { if let val = (self.config)[keyPath: KeyPath(configKey)] as? JSArray { return val From e456de083e19644f484bec5a5359cb67960ac8bc Mon Sep 17 00:00:00 2001 From: Joey Pender Date: Fri, 22 May 2026 13:39:38 -0500 Subject: [PATCH 09/30] fix(SystemBars): make `safe-area-inset-x` available on API <= 34 (#8424) --- .../com/getcapacitor/plugin/SystemBars.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) 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 b335b4b1c..76e74207c 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -164,13 +164,18 @@ private Insets calcSafeAreaInsets(WindowInsetsCompat insets) { } private void initSafeAreaCSSVariables() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && insetHandlingEnabled) { + WindowInsetsCompat insets; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { View v = (View) this.getBridge().getWebView().getParent(); - WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(v); - if (insets != null) { - Insets safeAreaInsets = calcSafeAreaInsets(insets); - injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); - } + insets = ViewCompat.getRootWindowInsets(v); + } else { + insets = WindowInsetsCompat.CONSUMED; + } + + if (insets != null) { + Insets safeAreaInsets = calcSafeAreaInsets(insets); + injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); } } @@ -186,10 +191,8 @@ private void initWindowInsetsListener() { // We need to correct for a possible shown IME v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && hasViewportCover && insetHandlingEnabled) { - Insets safeAreaInsets = calcSafeAreaInsets(insets); - injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); - } + Insets safeAreaInsets = calcSafeAreaInsets(insets); + injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); return new WindowInsetsCompat.Builder(insets) .setInsets( @@ -221,10 +224,8 @@ private void initWindowInsetsListener() { .setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0)) .build(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && hasViewportCover && insetHandlingEnabled) { - Insets safeAreaInsets = calcSafeAreaInsets(newInsets); - injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); - } + Insets safeAreaInsets = calcSafeAreaInsets(newInsets); + injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); return newInsets; }); From 258867b7bf37b1837b99b02ec9638e5a6df08d97 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Tue, 26 May 2026 12:00:02 +0200 Subject: [PATCH 10/30] feat(cli): add experimental packageOptions (#8471) --- cli/src/declarations.ts | 20 ++++++++++++++++++++ cli/src/ios/update.ts | 3 +++ cli/src/util/spm.ts | 21 ++++++++++++++++++--- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index 6c8645406..bda7ff0c0 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -559,6 +559,13 @@ export interface CapacitorConfig { * @since 8.3.0 */ packageTraits?: { [pluginId: string]: string[] }; + /** + * Define options to apply to the package. + * The key is the plugin ID (e.g. `@capacitor-community/device`) + * + * @since 8.4.0 + */ + packageOptions?: { [pluginId: string]: PackageOptions }; }; }; }; @@ -799,3 +806,16 @@ export interface PluginsConfig { animation?: 'FADE' | 'NONE'; }; } + +export interface PackageOptions { + /** + * Create a symlink to the plugin folder instead of pointing to the plugin path. + * Useful when plugin names conflict. + */ + symlink?: boolean; + /** + * Useful to avoid target name conflicts in dependencies + * [see](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/modulealiasing/) + */ + moduleAliases?: { [target: string]: string }; +} diff --git a/cli/src/ios/update.ts b/cli/src/ios/update.ts index 82ca85a88..b060e59a8 100644 --- a/cli/src/ios/update.ts +++ b/cli/src/ios/update.ts @@ -639,6 +639,9 @@ async function copyPluginsNativeFiles(config: Config, cordovaPlugins: Plugin[]) async function removePluginsNativeFiles(config: Config) { await remove(config.ios.cordovaPluginsDirAbs); + if ((await config.ios.packageManager) === 'SPM') { + await remove(join(config.ios.nativeProjectDirAbs, 'CapApp-SPM', 'symlinks')); + } await extractTemplate(config.cli.assets.ios.cordovaPluginsTemplateArchiveAbs, config.ios.cordovaPluginsDirAbs); } diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts index 7d15787ce..2ef26bcda 100644 --- a/cli/src/util/spm.ts +++ b/cli/src/util/spm.ts @@ -1,4 +1,4 @@ -import { pathExists, existsSync, readFileSync, writeFileSync, remove, move, mkdtemp } from 'fs-extra'; +import { ensureSymlink, pathExists, existsSync, readFileSync, writeFileSync, remove, move, mkdtemp } from 'fs-extra'; import { tmpdir } from 'os'; import { join, relative, resolve } from 'path'; import type { PlistObject } from 'plist'; @@ -100,6 +100,7 @@ export async function generatePackageText(config: Config, plugins: Plugin[]): Pr const iosPlatformVersion = await getCapacitorPackageVersion(config, config.ios.name); const iosVersion = getMajoriOSVersion(config); const packageTraits = config.app.extConfig.experimental?.ios?.spm?.packageTraits ?? {}; + const packageOptions = config.app.extConfig.experimental?.ios?.spm?.packageOptions ?? {}; const swiftToolsVersion = config.app.extConfig.experimental?.ios?.spm?.swiftToolsVersion ?? '5.9'; let packageSwiftText = `// swift-tools-version: ${swiftToolsVersion} @@ -132,7 +133,13 @@ let package = Package( packageSwiftText += `,\n .package(name: "${plugin.name}", path: "../../capacitor-cordova-ios-plugins/sources/${plugin.name}")`; } } else { - const relPath = relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath); + const options = packageOptions[plugin.id]; + const symlink = options?.symlink; + const symlinkFolder = join('symlinks', plugin.name); + const relPath = symlink ? symlinkFolder : relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath); + if (symlink) { + await ensureSymlink(plugin.rootPath, resolve(config.ios.nativeProjectDirAbs, 'CapApp-SPM', symlinkFolder)); + } const traits = packageTraits[plugin.id]; const traitsSuffix = traits?.length ? `, traits: [${traits @@ -156,7 +163,15 @@ let package = Package( .product(name: "Cordova", package: "capacitor-swift-pm")`; for (const plugin of plugins) { - let pluginText = `,\n .product(name: "${plugin.ios?.name}", package: "${plugin.ios?.name}")`; + const aliases = Object.entries(packageOptions[plugin.id]?.moduleAliases ?? {}); + const aliasText = aliases?.length + ? `, moduleAliases: [${aliases + .map(([target, replacement]) => { + return `"${target}": "${replacement}"`; + }) + .join(', ')}]` + : ''; + let pluginText = `,\n .product(name: "${plugin.ios?.name}", package: "${plugin.ios?.name}"${aliasText})`; if (getPluginType(plugin, config.ios.name) === PluginType.Cordova) { const platformTag = getPluginPlatform(plugin, config.ios.name); if (platformTag.$?.package) { From b4b297a52f8732659662d5e5aaeff81c0f7d9835 Mon Sep 17 00:00:00 2001 From: "Alex J." Date: Wed, 27 May 2026 14:49:32 -0400 Subject: [PATCH 11/30] feat(cli): capture ios_package_manager in telemetry (#8482) --- cli/src/telemetry.ts | 25 +++++++++++++++- cli/test/telemetry.spec.ts | 60 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 cli/test/telemetry.spec.ts diff --git a/cli/src/telemetry.ts b/cli/src/telemetry.ts index 8e0d847ac..593397502 100644 --- a/cli/src/telemetry.ts +++ b/cli/src/telemetry.ts @@ -1,8 +1,9 @@ import { Command } from 'commander'; import Debug from 'debug'; +import { pathExists } from 'fs-extra'; import c from './colors'; -import type { Config } from './definitions'; +import type { Config, PackageManager } from './definitions'; import { send } from './ipc'; import { output } from './log'; import { readConfig, writeConfig } from './sysconfig'; @@ -26,6 +27,7 @@ export interface CommandMetricData { error: string | null; node_version: string; os: string; + ios_package_manager: PackageManager | 'unknown'; } export interface Metric { @@ -80,9 +82,12 @@ export function telemetryAction(config: Config, action: CommanderAction): Comman error: error ? (error.message ? error.message : String(error)) : null, node_version: process.version, os: config.cli.os, + ios_package_manager: await getIOSPackageManager(config), ...Object.fromEntries(versions), }; + debug('metric payload: %O', data); + if (isInteractive()) { let sysconfig = await readConfig(); @@ -126,6 +131,24 @@ export async function sendMetric( } } +/** + * Resolve the iOS dependency manager for telemetry. Returns 'unknown' for + * non-iOS projects or when detection throws, so callers never have to handle + * errors from this signal. + */ +export async function getIOSPackageManager(config: Config): Promise { + try { + if (!(await pathExists(config.ios.platformDirAbs))) { + return 'unknown'; + } + + return await config.ios.packageManager; + } catch (e) { + debug('Could not resolve iOS package manager for telemetry: %O', e); + return 'unknown'; + } +} + /** * Get a unique anonymous identifier for this app. */ diff --git a/cli/test/telemetry.spec.ts b/cli/test/telemetry.spec.ts new file mode 100644 index 000000000..412048527 --- /dev/null +++ b/cli/test/telemetry.spec.ts @@ -0,0 +1,60 @@ +import { pathExists } from 'fs-extra'; + +import type { Config, IOSConfig } from '../src/definitions'; +import { getIOSPackageManager } from '../src/telemetry'; + +jest.mock('fs-extra', () => ({ + pathExists: jest.fn(), +})); + +const mockedPathExists = pathExists as unknown as jest.Mock; + +function makeConfig(ios: Partial): Config { + return { ios } as unknown as Config; +} + +describe('getIOSPackageManager', () => { + beforeEach(() => { + mockedPathExists.mockReset(); + }); + + it('returns "unknown" when the iOS platform directory does not exist', async () => { + mockedPathExists.mockResolvedValueOnce(false); + + const result = await getIOSPackageManager( + makeConfig({ + platformDirAbs: '/tmp/no-such-dir/ios', + packageManager: Promise.resolve('SPM'), + }), + ); + + expect(result).toBe('unknown'); + expect(mockedPathExists).toHaveBeenCalledWith('/tmp/no-such-dir/ios'); + }); + + it('returns the resolved package manager when iOS is present', async () => { + mockedPathExists.mockResolvedValueOnce(true); + + const result = await getIOSPackageManager( + makeConfig({ + platformDirAbs: '/tmp/app/ios', + packageManager: Promise.resolve('SPM'), + }), + ); + + expect(result).toBe('SPM'); + }); + + it('returns "unknown" when packageManager resolution throws', async () => { + mockedPathExists.mockResolvedValueOnce(true); + + const result = await getIOSPackageManager( + makeConfig({ + platformDirAbs: '/tmp/app/ios', + packageManager: Promise.reject(new Error('boom')), + }), + ); + + expect(result).toBe('unknown'); + }); +}); From 1d031a4abec2c793079ba8897ad2e40c4cc6c7f9 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Thu, 28 May 2026 15:00:43 +0200 Subject: [PATCH 12/30] fix(cli): revert live reload config on failure (#8485) --- cli/src/tasks/run.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/src/tasks/run.ts b/cli/src/tasks/run.ts index 2abdb9cd2..439c29209 100644 --- a/cli/src/tasks/run.ts +++ b/cli/src/tasks/run.ts @@ -116,6 +116,9 @@ export async function runCommand( await sleepForever(); } } catch (e: any) { + if (options.liveReload) { + await CapLiveReloadHelper.revertCapConfigForLiveReload(); + } if (!isFatal(e)) { fatal(e.stack ?? e); } From f1077ef9645b1ada2dd2635e3c3220612bdb4da6 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Fri, 29 May 2026 11:02:23 +0200 Subject: [PATCH 13/30] chore: fix typo in declarations.ts (#8488) --- cli/src/declarations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index bda7ff0c0..1103b6585 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -604,7 +604,7 @@ export interface CapacitorConfig { * Configure the local scheme on Android. * * Custom schemes on Android are unable to change the URL path as of Webview 117. Changing this value from anything other than `http` or `https` can result in your - * application unable to resolve routing. If you must change this for some reason, consider using a hash-based url strategy, but there are no guarentees that this + * application unable to resolve routing. If you must change this for some reason, consider using a hash-based url strategy, but there are no guarantees that this * will continue to work long term as allowing non-standard schemes to modify query parameters and url fragments is only allowed for compatibility reasons. * https://ionic.io/blog/capacitor-android-customscheme-issue-with-chrome-117 * From d4ad7ffe39daf66e0cfc63af9028d5c05543bde7 Mon Sep 17 00:00:00 2001 From: Joey Pender Date: Mon, 1 Jun 2026 10:31:48 -0500 Subject: [PATCH 14/30] fix(SystemBars): respect `insetsHandling` disable (#8481) --- .../com/getcapacitor/plugin/SystemBars.java | 54 ++++++++++++------- 1 file changed, 35 insertions(+), 19 deletions(-) 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 76e74207c..29b76e2f2 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -16,6 +16,7 @@ import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsControllerCompat; import androidx.webkit.WebViewCompat; +import com.getcapacitor.Logger; import com.getcapacitor.Plugin; import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; @@ -32,6 +33,7 @@ public class SystemBars extends Plugin { static final String BAR_STATUS_BAR = "StatusBar"; static final String BAR_GESTURE_BAR = "NavigationBar"; + // TODO: In Cap 9, add an additional option "full" static final String INSETS_HANDLING_CSS = "css"; static final String INSETS_HANDLING_DISABLE = "disable"; @@ -53,7 +55,7 @@ function capacitorSystemBarsCheckMetaViewport() { capacitorSystemBarsCheckMetaViewport(); """; - private boolean insetHandlingEnabled = true; + private String insetsHandling = INSETS_HANDLING_CSS; private boolean hasViewportCover = false; private String currentStatusBarStyle = STYLE_DEFAULT; @@ -94,9 +96,15 @@ private void initSystemBars() { String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase(Locale.US); boolean hidden = getConfig().getBoolean("hidden", false); - String insetsHandling = getConfig().getString("insetsHandling", "css"); - if (insetsHandling.equals(INSETS_HANDLING_DISABLE)) { - insetHandlingEnabled = false; + String configuredInsetsHandling = getConfig().getString("insetsHandling", INSETS_HANDLING_CSS); + if (INSETS_HANDLING_CSS.equals(configuredInsetsHandling) || INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling)) { + insetsHandling = configuredInsetsHandling; + } else { + Logger.warn( + "SystemBars", + "Unknown insetsHandling value '" + configuredInsetsHandling + "'. Falling back to '" + INSETS_HANDLING_CSS + "'." + ); + insetsHandling = INSETS_HANDLING_CSS; } initWindowInsetsListener(); @@ -146,13 +154,15 @@ public void setAnimation(final PluginCall call) { @JavascriptInterface public void onDOMReady() { - getActivity().runOnUiThread(() -> { - this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> { - hasViewportCover = res.equals("true"); + if (INSETS_HANDLING_CSS.equals(insetsHandling)) { + getActivity().runOnUiThread(() -> { + this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> { + hasViewportCover = res.equals("true"); - getBridge().getWebView().requestApplyInsets(); + getBridge().getWebView().requestApplyInsets(); + }); }); - }); + } } private Insets calcSafeAreaInsets(WindowInsetsCompat insets) { @@ -164,22 +174,28 @@ private Insets calcSafeAreaInsets(WindowInsetsCompat insets) { } private void initSafeAreaCSSVariables() { - WindowInsetsCompat insets; + if (INSETS_HANDLING_CSS.equals(insetsHandling)) { + WindowInsetsCompat insets; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - View v = (View) this.getBridge().getWebView().getParent(); - insets = ViewCompat.getRootWindowInsets(v); - } else { - insets = WindowInsetsCompat.CONSUMED; - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + View v = (View) this.getBridge().getWebView().getParent(); + insets = ViewCompat.getRootWindowInsets(v); + } else { + insets = WindowInsetsCompat.CONSUMED; + } - if (insets != null) { - Insets safeAreaInsets = calcSafeAreaInsets(insets); - injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); + if (insets != null) { + Insets safeAreaInsets = calcSafeAreaInsets(insets); + injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); + } } } private void initWindowInsetsListener() { + if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) { + return; + } + ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> { boolean shouldPassthroughInsets = getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover; From 4c6c3219afb5223211e857457e46283c37eb9424 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Mon, 1 Jun 2026 21:04:08 +0200 Subject: [PATCH 15/30] fix(android): show only the requested system bar (#8480) Co-authored-by: Joey Pender Co-authored-by: Pedro Bilro Co-authored-by: Mark Anderson --- .../com/getcapacitor/plugin/SystemBars.java | 14 ++-- .../getcapacitor/plugin/SystemBarsTest.java | 73 +++++++++++++++++++ 2 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java 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 29b76e2f2..fd7137094 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -304,19 +304,21 @@ private void setHidden(boolean hide, String bar) { WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, window.getDecorView()); if (hide) { - if (bar.isEmpty() || bar.equals(BAR_STATUS_BAR)) { + if (bar.isEmpty()) { + windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars()); + } else if (bar.equals(BAR_STATUS_BAR)) { windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.statusBars()); - } - if (bar.isEmpty() || bar.equals(BAR_GESTURE_BAR)) { + } else if (bar.equals(BAR_GESTURE_BAR)) { windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.navigationBars()); } return; } - if (bar.isEmpty() || bar.equals(BAR_STATUS_BAR)) { + if (bar.isEmpty()) { windowInsetsControllerCompat.show(WindowInsetsCompat.Type.systemBars()); - } - if (bar.isEmpty() || bar.equals(BAR_GESTURE_BAR)) { + } else if (bar.equals(BAR_STATUS_BAR)) { + windowInsetsControllerCompat.show(WindowInsetsCompat.Type.statusBars()); + } else if (bar.equals(BAR_GESTURE_BAR)) { windowInsetsControllerCompat.show(WindowInsetsCompat.Type.navigationBars()); } } diff --git a/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java b/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java new file mode 100644 index 000000000..f72373a88 --- /dev/null +++ b/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java @@ -0,0 +1,73 @@ +package com.getcapacitor.plugin; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.view.View; +import android.view.Window; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; +import com.getcapacitor.Bridge; +import java.lang.reflect.Method; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class SystemBarsTest { + + @Test + public void showWithEmptyBarShowsSystemBars() throws Exception { + WindowInsetsControllerCompat controller = invokeSetHidden(""); + + verify(controller).show(WindowInsetsCompat.Type.systemBars()); + verify(controller, never()).show(WindowInsetsCompat.Type.statusBars()); + verify(controller, never()).show(WindowInsetsCompat.Type.navigationBars()); + } + + @Test + public void showWithStatusBarShowsOnlyStatusBars() throws Exception { + WindowInsetsControllerCompat controller = invokeSetHidden("StatusBar"); + + verify(controller).show(WindowInsetsCompat.Type.statusBars()); + verify(controller, never()).show(WindowInsetsCompat.Type.systemBars()); + verify(controller, never()).show(WindowInsetsCompat.Type.navigationBars()); + } + + @Test + public void showWithNavigationBarShowsOnlyNavigationBars() throws Exception { + WindowInsetsControllerCompat controller = invokeSetHidden("NavigationBar"); + + verify(controller).show(WindowInsetsCompat.Type.navigationBars()); + verify(controller, never()).show(WindowInsetsCompat.Type.systemBars()); + verify(controller, never()).show(WindowInsetsCompat.Type.statusBars()); + } + + private WindowInsetsControllerCompat invokeSetHidden(String bar) throws Exception { + SystemBars plugin = new SystemBars(); + Bridge bridge = mock(Bridge.class); + AppCompatActivity activity = mock(AppCompatActivity.class); + Window window = mock(Window.class); + View decorView = mock(View.class); + WindowInsetsControllerCompat controller = mock(WindowInsetsControllerCompat.class); + + when(bridge.getActivity()).thenReturn(activity); + when(activity.getWindow()).thenReturn(window); + when(window.getDecorView()).thenReturn(decorView); + + plugin.setBridge(bridge); + + try (MockedStatic windowCompat = mockStatic(WindowCompat.class)) { + windowCompat.when(() -> WindowCompat.getInsetsController(window, decorView)).thenReturn(controller); + + Method setHidden = SystemBars.class.getDeclaredMethod("setHidden", boolean.class, String.class); + setHidden.setAccessible(true); + setHidden.invoke(plugin, false, bar); + } + + return controller; + } +} From 36b729d10380687bf5eab2cfa8affcbd1655541b Mon Sep 17 00:00:00 2001 From: Mark Anderson Date: Mon, 1 Jun 2026 15:11:51 -0400 Subject: [PATCH 16/30] chore: Decrease timeout for CI jobs from 60 to 30 minutes (#8476) Co-authored-by: jcesarmobile --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8664a402d..56fce5389 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ concurrency: jobs: setup: runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 30 steps: - name: Get Latest uses: actions/setup-node@v6 @@ -32,7 +32,7 @@ jobs: key: ${{ runner.OS }}-dependencies-cache-${{ hashFiles('**/package.json') }} lint: runs-on: macos-15 - timeout-minutes: 60 + timeout-minutes: 30 steps: - uses: actions/setup-node@v6 with: @@ -48,7 +48,7 @@ jobs: - run: npm run lint test-cli: runs-on: macos-15 - timeout-minutes: 60 + timeout-minutes: 30 needs: - setup - lint @@ -69,7 +69,7 @@ jobs: working-directory: ./cli test-core: runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 30 needs: - setup - lint @@ -90,7 +90,7 @@ jobs: working-directory: ./core test-ios: runs-on: macos-15 - timeout-minutes: 60 + timeout-minutes: 30 needs: - setup - lint @@ -118,7 +118,7 @@ jobs: run: sh ./scripts/native-podspec.sh lint test-android: runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 30 needs: - setup - lint From 41fd9deb6df07ef83ae390d16824e8bcbefef517 Mon Sep 17 00:00:00 2001 From: "Github Workflow (on behalf of markemer)" Date: Tue, 2 Jun 2026 15:56:03 +0000 Subject: [PATCH 17/30] Release 8.4.0 --- CHANGELOG.md | 15 +++++++++++++++ android/CHANGELOG.md | 12 ++++++++++++ android/package.json | 4 ++-- cli/CHANGELOG.md | 11 +++++++++++ cli/package.json | 2 +- core/CHANGELOG.md | 4 ++++ core/package.json | 2 +- ios/CHANGELOG.md | 6 ++++++ ios/package.json | 4 ++-- lerna.json | 2 +- 10 files changed, 55 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f2b7713..cd4fe5f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) + +### 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 + +- 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)) +- **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)) + ## [8.3.4](https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4) (2026-05-12) **Note:** Version bump only for package capacitor diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 74d538d9f..df7ce545e 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) + +### 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)) +- **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 + +- add method getDouble to plugin config ([#7638](https://github.com/ionic-team/capacitor/issues/7638)) ([93c72de](https://github.com/ionic-team/capacitor/commit/93c72de40a2ec4c78b33659250cb08340083088e)) + ## [8.3.4](https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4) (2026-05-12) **Note:** Version bump only for package @capacitor/android diff --git a/android/package.json b/android/package.json index 8ae93f3f2..af3f4f556 100644 --- a/android/package.json +++ b/android/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/android", - "version": "8.3.4", + "version": "8.4.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", @@ -23,7 +23,7 @@ "verify": "./gradlew clean lint build test -b capacitor/build.gradle" }, "peerDependencies": { - "@capacitor/core": "^8.3.0" + "@capacitor/core": "^8.4.0" }, "publishConfig": { "access": "public" diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index bba24688f..ee48a8d5f 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) + +### 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 + +- **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)) + ## [8.3.4](https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4) (2026-05-12) **Note:** Version bump only for package @capacitor/cli diff --git a/cli/package.json b/cli/package.json index 9df346b14..1f987d33a 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/cli", - "version": "8.3.4", + "version": "8.4.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 017293794..1de9522ff 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) + +**Note:** Version bump only for package @capacitor/core + ## [8.3.4](https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4) (2026-05-12) **Note:** Version bump only for package @capacitor/core diff --git a/core/package.json b/core/package.json index eef113a50..b22aef174 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/core", - "version": "8.3.4", + "version": "8.4.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 87ee73bb4..eac0a4a1c 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) + +### 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)) + ## [8.3.4](https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4) (2026-05-12) **Note:** Version bump only for package @capacitor/ios diff --git a/ios/package.json b/ios/package.json index a7b8d3133..a58c02090 100644 --- a/ios/package.json +++ b/ios/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/ios", - "version": "8.3.4", + "version": "8.4.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", @@ -25,7 +25,7 @@ "xc:build:CapacitorCordova": "cd CapacitorCordova && xcodebuild && cd .." }, "peerDependencies": { - "@capacitor/core": "^8.3.0" + "@capacitor/core": "^8.4.0" }, "publishConfig": { "access": "public" diff --git a/lerna.json b/lerna.json index 1e53d6bdc..b2efb400d 100644 --- a/lerna.json +++ b/lerna.json @@ -11,6 +11,6 @@ "tagVersionPrefix": "" } }, - "version": "8.3.4", + "version": "8.4.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } From 28bb2c687069dfdd6aa7abc866004a1c6388d103 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Tue, 2 Jun 2026 21:31:41 +0200 Subject: [PATCH 18/30] fix(cli): patch Capacitor SPM dependency version in plugins (#8492) Co-authored-by: Mark Anderson --- cli/src/ios/update.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/cli/src/ios/update.ts b/cli/src/ios/update.ts index b060e59a8..1f4bc87d0 100644 --- a/cli/src/ios/update.ts +++ b/cli/src/ios/update.ts @@ -1,5 +1,6 @@ import { copy, remove, pathExists, readFile, realpath, writeFile } from 'fs-extra'; import { basename, dirname, join, relative } from 'path'; +import { major } from 'semver'; import c from '../colors'; import { checkPlatformVersions, getCapacitorPackageVersion, runTask } from '../common'; @@ -58,6 +59,28 @@ 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) { + content = setAllStringIn( + content, + `url: "https://github.com/ionic-team/capacitor-swift-pm.git",`, + `)`, + ` from: "${majorCapVersion}.0.0"`, + ); + await writeFile(packageSwiftPath, content); + logger.warn(`${plugin.id} is built for Capacitor ${major(version)}, it might cause issues`); + } + }), + ); await generatePackageFile(config, validSPMPackages.concat(cordovaPlugins)); } else { From 6048e90171afa0229a3c25b52a23c377c6bb804c Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Mon, 15 Jun 2026 12:11:19 +0200 Subject: [PATCH 19/30] fix(cli): make SPM dependency patch work on prereleases (#8508) --- cli/src/ios/update.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/src/ios/update.ts b/cli/src/ios/update.ts index 1f4bc87d0..3ab793c25 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 } from 'semver'; +import { major, prerelease } from 'semver'; import c from '../colors'; import { checkPlatformVersions, getCapacitorPackageVersion, runTask } from '../common'; @@ -70,11 +70,13 @@ async function updatePluginFiles(config: Config, plugins: Plugin[], deployment: 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: "${majorCapVersion}.0.0"`, + ` from: "${forceVersion}"`, ); await writeFile(packageSwiftPath, content); logger.warn(`${plugin.id} is built for Capacitor ${major(version)}, it might cause issues`); From 7217b5215a175eededf607a216e0cab2a8450a34 Mon Sep 17 00:00:00 2001 From: "Github Workflow (on behalf of jcesarmobile)" Date: Fri, 19 Jun 2026 09:01:59 +0000 Subject: [PATCH 20/30] Release 8.4.1 --- CHANGELOG.md | 7 +++++++ android/CHANGELOG.md | 4 ++++ android/package.json | 2 +- cli/CHANGELOG.md | 7 +++++++ cli/package.json | 2 +- core/CHANGELOG.md | 4 ++++ core/package.json | 2 +- ios/CHANGELOG.md | 4 ++++ ios/package.json | 2 +- lerna.json | 2 +- 10 files changed, 31 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd4fe5f5a..db3954562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.4.1](https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1) (2026-06-19) + +### Bug Fixes + +- **cli:** make SPM dependency patch work on prereleases ([#8508](https://github.com/ionic-team/capacitor/issues/8508)) ([6048e90](https://github.com/ionic-team/capacitor/commit/6048e90171afa0229a3c25b52a23c377c6bb804c)) +- **cli:** patch Capacitor SPM dependency version in plugins ([#8492](https://github.com/ionic-team/capacitor/issues/8492)) ([28bb2c6](https://github.com/ionic-team/capacitor/commit/28bb2c687069dfdd6aa7abc866004a1c6388d103)) + # [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) ### Bug Fixes diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index df7ce545e..5bc9de9f6 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.4.1](https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1) (2026-06-19) + +**Note:** Version bump only for package @capacitor/android + # [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) ### Bug Fixes diff --git a/android/package.json b/android/package.json index af3f4f556..7354f3b2c 100644 --- a/android/package.json +++ b/android/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/android", - "version": "8.4.0", + "version": "8.4.1", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index ee48a8d5f..df61fcbaa 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.4.1](https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1) (2026-06-19) + +### Bug Fixes + +- **cli:** make SPM dependency patch work on prereleases ([#8508](https://github.com/ionic-team/capacitor/issues/8508)) ([6048e90](https://github.com/ionic-team/capacitor/commit/6048e90171afa0229a3c25b52a23c377c6bb804c)) +- **cli:** patch Capacitor SPM dependency version in plugins ([#8492](https://github.com/ionic-team/capacitor/issues/8492)) ([28bb2c6](https://github.com/ionic-team/capacitor/commit/28bb2c687069dfdd6aa7abc866004a1c6388d103)) + # [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) ### Bug Fixes diff --git a/cli/package.json b/cli/package.json index 1f987d33a..7a77d11e9 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/cli", - "version": "8.4.0", + "version": "8.4.1", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 1de9522ff..1003c8ceb 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.4.1](https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1) (2026-06-19) + +**Note:** Version bump only for package @capacitor/core + # [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) **Note:** Version bump only for package @capacitor/core diff --git a/core/package.json b/core/package.json index b22aef174..f821cfd9d 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/core", - "version": "8.4.0", + "version": "8.4.1", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index eac0a4a1c..dfcf383ff 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.4.1](https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1) (2026-06-19) + +**Note:** Version bump only for package @capacitor/ios + # [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) ### Features diff --git a/ios/package.json b/ios/package.json index a58c02090..a6a78c23d 100644 --- a/ios/package.json +++ b/ios/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/ios", - "version": "8.4.0", + "version": "8.4.1", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/lerna.json b/lerna.json index b2efb400d..be3666942 100644 --- a/lerna.json +++ b/lerna.json @@ -11,6 +11,6 @@ "tagVersionPrefix": "" } }, - "version": "8.4.0", + "version": "8.4.1", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } From b789b683f04863e65c66d630890f00561c0d5b7a Mon Sep 17 00:00:00 2001 From: Mark Anderson Date: Mon, 22 Jun 2026 15:42:23 -0400 Subject: [PATCH 21/30] chore: run `npm run fmt` to fix lint errors (#8516) --- .../java/com/getcapacitor/BridgeWebChromeClient.java | 4 ++-- .../src/main/java/com/getcapacitor/FileUtils.java | 2 +- .../java/com/getcapacitor/WebViewLocalServer.java | 12 ++++++------ .../getcapacitor/plugin/CapacitorCookieManager.java | 6 +++--- .../getcapacitor/plugin/util/HttpRequestHandler.java | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java index 6ca1c8a83..ec7867369 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java +++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java @@ -282,7 +282,7 @@ public boolean onShowFileChooser( boolean captureEnabled = fileChooserParams.isCaptureEnabled(); boolean capturePhoto = captureEnabled && acceptTypes.contains("image/*"); final boolean captureVideo = captureEnabled && acceptTypes.contains("video/*"); - if ((capturePhoto || captureVideo)) { + if (capturePhoto || captureVideo) { if (isMediaCaptureSupported()) { showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo); } else { @@ -447,7 +447,7 @@ public boolean onConsoleMessage(ConsoleMessage consoleMessage) { } public boolean isValidMsg(String msg) { - return !(msg.contains("%cresult %c") || (msg.contains("%cnative %c")) || msg.equalsIgnoreCase("console.groupEnd")); + return !(msg.contains("%cresult %c") || msg.contains("%cnative %c") || msg.equalsIgnoreCase("console.groupEnd")); } private Uri createImageFileUri() throws IOException { diff --git a/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java b/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java index ce09e7c2f..cec171946 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java +++ b/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java @@ -218,7 +218,7 @@ private static String getCopyFilePath(Uri uri, Context context) { Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); cursor.moveToFirst(); - String name = (cursor.getString(nameIndex)); + String name = cursor.getString(nameIndex); String fileName = sanitizeFilename(name); File file = new File(context.getFilesDir(), fileName); try { diff --git a/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java b/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java index a39483de9..0c5480079 100755 --- a/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java +++ b/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java @@ -221,7 +221,7 @@ private boolean isErrorUrl(Uri uri) { } private boolean isMainUrl(Uri loadingUrl) { - return (bridge.getServerUrl() == null && loadingUrl.getHost().equalsIgnoreCase(bridge.getHost())); + return bridge.getServerUrl() == null && loadingUrl.getHost().equalsIgnoreCase(bridge.getHost()); } private boolean isAllowedUrl(Uri loadingUrl) { @@ -722,31 +722,31 @@ private InputStream getInputStream() { @Override public int available() throws IOException { InputStream is = getInputStream(); - return (is != null) ? is.available() : -1; + return is != null ? is.available() : -1; } @Override public int read() throws IOException { InputStream is = getInputStream(); - return (is != null) ? is.read() : -1; + return is != null ? is.read() : -1; } @Override public int read(byte[] b) throws IOException { InputStream is = getInputStream(); - return (is != null) ? is.read(b) : -1; + return is != null ? is.read(b) : -1; } @Override public int read(byte[] b, int off, int len) throws IOException { InputStream is = getInputStream(); - return (is != null) ? is.read(b, off, len) : -1; + return is != null ? is.read(b, off, len) : -1; } @Override public long skip(long n) throws IOException { InputStream is = getInputStream(); - return (is != null) ? is.skip(n) : 0; + return is != null ? is.skip(n) : 0; } } diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java index cf4ab6326..472323bbc 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java @@ -192,12 +192,12 @@ public void flush() { @Override public void put(URI uri, Map> responseHeaders) { // make sure our args are valid - if ((uri == null) || (responseHeaders == null)) return; + if (uri == null || responseHeaders == null) return; // go over the headers for (String headerKey : responseHeaders.keySet()) { // ignore headers which aren't cookie related - if ((headerKey == null) || !(headerKey.equalsIgnoreCase("Set-Cookie2") || headerKey.equalsIgnoreCase("Set-Cookie"))) continue; + if (headerKey == null || !(headerKey.equalsIgnoreCase("Set-Cookie2") || headerKey.equalsIgnoreCase("Set-Cookie"))) continue; // process each of the headers for (String headerValue : Objects.requireNonNull(responseHeaders.get(headerKey))) { @@ -215,7 +215,7 @@ public void put(URI uri, Map> responseHeaders) { @Override public Map> get(URI uri, Map> requestHeaders) { // make sure our args are valid - if ((uri == null) || (requestHeaders == null)) throw new IllegalArgumentException("Argument is null"); + if (uri == null || requestHeaders == null) throw new IllegalArgumentException("Argument is null"); // save our url once String url = uri.toString(); diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java index b47d57cf1..69278bc63 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java @@ -168,8 +168,8 @@ public HttpURLConnectionBuilder setUrlParams(JSObject params, boolean shouldEnco "://" + uri.getAuthority() + uri.getPath() + - ((!urlQuery.equals("")) ? "?" + urlQuery : "") + - ((uri.getFragment() != null) ? uri.getFragment() : ""); + (!urlQuery.equals("") ? "?" + urlQuery : "") + + (uri.getFragment() != null ? uri.getFragment() : ""); this.url = new URL(unEncodedUrlString); return this; From 6f2d3283897a375d09ca9ec8784a9f0d65f0a530 Mon Sep 17 00:00:00 2001 From: "Alex J." Date: Wed, 8 Jul 2026 10:35:37 -0400 Subject: [PATCH 22/30] fix(android): explicitly grant URI permissions for image capture intent (#8526) --- .../src/main/java/com/getcapacitor/BridgeWebChromeClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java index ec7867369..34fa2d834 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java +++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java @@ -340,6 +340,7 @@ private boolean showImageCapturePicker(final ValueCallback filePathCallba return false; } takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri); + takePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); activityListener = (activityResult) -> { Uri[] result = null; if (activityResult.getResultCode() == Activity.RESULT_OK) { From 1834b9740e200fd96f866975d54515d49ef1aa97 Mon Sep 17 00:00:00 2001 From: "Github Workflow (on behalf of markemer)" Date: Tue, 14 Jul 2026 17:32:29 +0000 Subject: [PATCH 23/30] Release 8.4.2 --- CHANGELOG.md | 6 ++++++ android/CHANGELOG.md | 6 ++++++ android/package.json | 2 +- cli/CHANGELOG.md | 4 ++++ cli/package.json | 2 +- core/CHANGELOG.md | 4 ++++ core/package.json | 2 +- ios/CHANGELOG.md | 4 ++++ ios/package.json | 2 +- lerna.json | 2 +- 10 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db3954562..41320fefa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.4.2](https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2) (2026-07-14) + +### Bug Fixes + +- **android:** explicitly grant URI permissions for image capture intent ([#8526](https://github.com/ionic-team/capacitor/issues/8526)) ([6f2d328](https://github.com/ionic-team/capacitor/commit/6f2d3283897a375d09ca9ec8784a9f0d65f0a530)) + ## [8.4.1](https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1) (2026-06-19) ### Bug Fixes diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 5bc9de9f6..106d9c0c8 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.4.2](https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2) (2026-07-14) + +### Bug Fixes + +- **android:** explicitly grant URI permissions for image capture intent ([#8526](https://github.com/ionic-team/capacitor/issues/8526)) ([6f2d328](https://github.com/ionic-team/capacitor/commit/6f2d3283897a375d09ca9ec8784a9f0d65f0a530)) + ## [8.4.1](https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1) (2026-06-19) **Note:** Version bump only for package @capacitor/android diff --git a/android/package.json b/android/package.json index 7354f3b2c..285d9ec78 100644 --- a/android/package.json +++ b/android/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/android", - "version": "8.4.1", + "version": "8.4.2", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index df61fcbaa..3fb75e17a 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.4.2](https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2) (2026-07-14) + +**Note:** Version bump only for package @capacitor/cli + ## [8.4.1](https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1) (2026-06-19) ### Bug Fixes diff --git a/cli/package.json b/cli/package.json index 7a77d11e9..e0c99f6d0 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/cli", - "version": "8.4.1", + "version": "8.4.2", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 1003c8ceb..a90269b59 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.4.2](https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2) (2026-07-14) + +**Note:** Version bump only for package @capacitor/core + ## [8.4.1](https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1) (2026-06-19) **Note:** Version bump only for package @capacitor/core diff --git a/core/package.json b/core/package.json index f821cfd9d..870ee47a7 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/core", - "version": "8.4.1", + "version": "8.4.2", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index dfcf383ff..9a30e6bb4 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.4.2](https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2) (2026-07-14) + +**Note:** Version bump only for package @capacitor/ios + ## [8.4.1](https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1) (2026-06-19) **Note:** Version bump only for package @capacitor/ios diff --git a/ios/package.json b/ios/package.json index a6a78c23d..5e0fbed66 100644 --- a/ios/package.json +++ b/ios/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/ios", - "version": "8.4.1", + "version": "8.4.2", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/lerna.json b/lerna.json index be3666942..fbd4baa8e 100644 --- a/lerna.json +++ b/lerna.json @@ -11,6 +11,6 @@ "tagVersionPrefix": "" } }, - "version": "8.4.1", + "version": "8.4.2", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } From f368a1b1ec90394ff44dd3b9ecea9ea6dfb225c1 Mon Sep 17 00:00:00 2001 From: Pedro Bilro Date: Mon, 27 Jul 2026 18:26:32 +0100 Subject: [PATCH 24/30] chore(android): fix lint issues (#8542) --- .../com/getcapacitor/plugin/SystemBars.java | 20 +++++++++---------- .../plugin/util/HttpRequestHandlerTest.java | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) 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 fd7137094..b3c95916a 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -43,17 +43,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; 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 fd8b686ee..100672123 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 { From 4c1c8709413b9c19b008c99122ca330cc3c90e6f Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Tue, 28 Jul 2026 09:20:15 -0700 Subject: [PATCH 25/30] fix(cli): support TypeScript 7 when loading capacitor.config.ts (#8534) --- cli/src/config.ts | 2 +- cli/src/util/node.ts | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/cli/src/config.ts b/cli/src/config.ts index 4e71fb01d..bb71177dc 100644 --- a/cli/src/config.ts +++ b/cli/src/config.ts @@ -113,7 +113,7 @@ async function loadExtConfigTS( } const ts = require(tsPath); // eslint-disable-line @typescript-eslint/no-var-requires - const extConfigObject = requireTS(ts, extConfigFilePath) as any; + const extConfigObject = (await requireTS(ts, extConfigFilePath)) as any; const extConfig = extConfigObject.default ? await extConfigObject.default : extConfigObject; return { diff --git a/cli/src/util/node.ts b/cli/src/util/node.ts index 299dffdd3..6f511da25 100644 --- a/cli/src/util/node.ts +++ b/cli/src/util/node.ts @@ -2,17 +2,55 @@ import { existsSync } from 'fs'; import { readFileSync } from 'fs-extra'; import { resolve } from 'path'; import type typescript from 'typescript'; +import { pathToFileURL } from 'url'; interface NodeModuleWithCompile extends NodeJS.Module { _compile?(code: string, filename: string): any; } +/** + * TypeScript 7 dropped the classic compiler API (transpileModule, ModuleKind, etc.) from its + * default export -- `require('typescript')` now only exposes `version`/`versionMajorMinor`. + * Detect that case so we can fall back to a loading strategy that doesn't depend on it. + * + * @see https://github.com/ionic-team/capacitor/issues/8531 + */ +function hasClassicCompilerAPI(ts: typeof typescript): boolean { + return typeof ts.transpileModule === 'function'; +} + +/** + * A plain `import()` here gets downleveled to a `require()` call by tsc when compiling to + * CommonJS (our build target), which defeats the purpose -- we specifically need the real ESM + * loader so Node's native TypeScript stripping kicks in. Constructing the import from a string + * keeps tsc from touching it. + */ +const dynamicImport: (specifier: string) => Promise = new Function('specifier', 'return import(specifier)') as any; + /** * @see https://github.com/ionic-team/stencil/blob/HEAD/src/compiler/sys/node-require.ts */ -export const requireTS = (ts: typeof typescript, p: 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) => { From 3fa04a357c92af34cd6fccb8124791963804a9dc Mon Sep 17 00:00:00 2001 From: Joey Pender Date: Wed, 29 Jul 2026 15:17:46 -0500 Subject: [PATCH 26/30] feat(ios): UIScene Support (#8536) --- .../App/App.xcodeproj/project.pbxproj | 4 + ios-pods-template/App/App/AppDelegate.swift | 9 ++ ios-pods-template/App/App/Info.plist | 19 ++++ ios-pods-template/App/App/SceneDelegate.swift | 24 +++++ .../App/App.xcodeproj/project.pbxproj | 4 + ios-spm-template/App/App/AppDelegate.swift | 19 ++-- ios-spm-template/App/App/Info.plist | 21 ++++- ios-spm-template/App/App/SceneDelegate.swift | 24 +++++ .../Capacitor.xcodeproj/project.pbxproj | 12 ++- .../CAPApplicationDelegateProxy.swift | 2 +- .../Capacitor/CAPNotifications.swift | 7 ++ .../Capacitor/CAPSceneDelegateProxy.swift | 87 +++++++++++++++++++ ios/Capacitor/Capacitor/CapacitorBridge.swift | 20 ++--- .../Capacitor/TmpViewController.swift | 8 -- .../Capacitor/WebViewDelegationHandler.swift | 2 +- ios/Capacitor/TestsHostApp/AppDelegate.swift | 9 ++ .../TestsHostApp/SceneDelegate.swift | 5 ++ 17 files changed, 238 insertions(+), 38 deletions(-) create mode 100644 ios-pods-template/App/App/SceneDelegate.swift create mode 100644 ios-spm-template/App/App/SceneDelegate.swift create mode 100644 ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift delete mode 100644 ios/Capacitor/Capacitor/TmpViewController.swift create mode 100644 ios/Capacitor/TestsHostApp/SceneDelegate.swift diff --git a/ios-pods-template/App/App.xcodeproj/project.pbxproj b/ios-pods-template/App/App.xcodeproj/project.pbxproj index 092f57491..75dc9750f 100644 --- a/ios-pods-template/App/App.xcodeproj/project.pbxproj +++ b/ios-pods-template/App/App.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; + 9582B6852FE996820072D4E8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9582B6842FE996800072D4E8 /* SceneDelegate.swift */; }; A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; }; /* End PBXBuildFile section */ @@ -27,6 +28,7 @@ 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + 9582B6842FE996800072D4E8 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; @@ -73,6 +75,7 @@ 504EC3061FED79650016851F /* App */ = { isa = PBXGroup; children = ( + 9582B6842FE996800072D4E8 /* SceneDelegate.swift */, 50379B222058CBB4000EE86E /* capacitor.config.json */, 504EC3071FED79650016851F /* AppDelegate.swift */, 504EC30B1FED79650016851F /* Main.storyboard */, @@ -210,6 +213,7 @@ buildActionMask = 2147483647; files = ( 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + 9582B6852FE996820072D4E8 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios-pods-template/App/App/AppDelegate.swift b/ios-pods-template/App/App/AppDelegate.swift index c3cd83b5c..7ad11d83e 100644 --- a/ios-pods-template/App/App/AppDelegate.swift +++ b/ios-pods-template/App/App/AppDelegate.swift @@ -46,4 +46,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) } + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + + let config = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + config.delegateClass = SceneDelegate.self + return config + } + } diff --git a/ios-pods-template/App/App/Info.plist b/ios-pods-template/App/App/Info.plist index ed6ff0bd4..f8c8a5765 100644 --- a/ios-pods-template/App/App/Info.plist +++ b/ios-pods-template/App/App/Info.plist @@ -22,6 +22,25 @@ $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/ios-pods-template/App/App/SceneDelegate.swift b/ios-pods-template/App/App/SceneDelegate.swift new file mode 100644 index 000000000..f352e1e95 --- /dev/null +++ b/ios-pods-template/App/App/SceneDelegate.swift @@ -0,0 +1,24 @@ +import UIKit +import Capacitor + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { return } + + window = UIWindow(windowScene: windowScene) + window?.rootViewController = CAPBridgeViewController() + window?.makeKeyAndVisible() + + SceneDelegateProxy.shared.scene(scene, willConnectTo: session, options: connectionOptions) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + SceneDelegateProxy.shared.scene(scene, openURLContexts: URLContexts) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + SceneDelegateProxy.shared.scene(scene, continue: userActivity) + } +} diff --git a/ios-spm-template/App/App.xcodeproj/project.pbxproj b/ios-spm-template/App/App.xcodeproj/project.pbxproj index 46f934efd..56a0b7d40 100644 --- a/ios-spm-template/App/App.xcodeproj/project.pbxproj +++ b/ios-spm-template/App/App.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; + 9582B6832FE993A70072D4E8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -27,6 +28,7 @@ 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ @@ -62,6 +64,7 @@ 504EC3061FED79650016851F /* App */ = { isa = PBXGroup; children = ( + 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */, 50379B222058CBB4000EE86E /* capacitor.config.json */, 504EC3071FED79650016851F /* AppDelegate.swift */, 504EC30B1FED79650016851F /* Main.storyboard */, @@ -156,6 +159,7 @@ buildActionMask = 2147483647; files = ( 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + 9582B6832FE993A70072D4E8 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios-spm-template/App/App/AppDelegate.swift b/ios-spm-template/App/App/AppDelegate.swift index c3cd83b5c..fec95acae 100644 --- a/ios-spm-template/App/App/AppDelegate.swift +++ b/ios-spm-template/App/App/AppDelegate.swift @@ -33,17 +33,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - // Called when the app was launched with a url. Feel free to add additional processing here, - // but if you want the App API to support tracking app url opens, make sure to keep this call - return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + let config = UISceneConfiguration(name: "Default Configuration", + sessionRole: connectingSceneSession.role) + config.delegateClass = SceneDelegate.self + return config } - - func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - // Called when the app was launched with an activity, including Universal Links. - // Feel free to add additional processing here, but if you want the App API to support - // tracking app url opens, make sure to keep this call - return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) - } - } diff --git a/ios-spm-template/App/App/Info.plist b/ios-spm-template/App/App/Info.plist index 42bcf6750..fc41ce69b 100644 --- a/ios-spm-template/App/App/Info.plist +++ b/ios-spm-template/App/App/Info.plist @@ -2,7 +2,7 @@ - CAPACITOR_DEBUG + CAPACITOR_DEBUG $(CAPACITOR_DEBUG) CFBundleDevelopmentRegion en @@ -24,6 +24,25 @@ $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/ios-spm-template/App/App/SceneDelegate.swift b/ios-spm-template/App/App/SceneDelegate.swift new file mode 100644 index 000000000..f352e1e95 --- /dev/null +++ b/ios-spm-template/App/App/SceneDelegate.swift @@ -0,0 +1,24 @@ +import UIKit +import Capacitor + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { return } + + window = UIWindow(windowScene: windowScene) + window?.rootViewController = CAPBridgeViewController() + window?.makeKeyAndVisible() + + SceneDelegateProxy.shared.scene(scene, willConnectTo: session, options: connectionOptions) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + SceneDelegateProxy.shared.scene(scene, openURLContexts: URLContexts) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + SceneDelegateProxy.shared.scene(scene, continue: userActivity) + } +} diff --git a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj index b060d9f7d..573e90cd9 100644 --- a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj +++ b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj @@ -52,7 +52,6 @@ 62959B3A2524DA7800A3D7F1 /* CAPLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62959B082524DA7700A3D7F1 /* CAPLog.swift */; }; 62959B3B2524DA7800A3D7F1 /* CAPPluginMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 62959B092524DA7700A3D7F1 /* CAPPluginMethod.h */; settings = {ATTRIBUTES = (Public, ); }; }; 62959B3C2524DA7800A3D7F1 /* CAPBridgeDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62959B0A2524DA7700A3D7F1 /* CAPBridgeDelegate.swift */; }; - 62959B402524DA7800A3D7F1 /* TmpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62959B0E2524DA7700A3D7F1 /* TmpViewController.swift */; }; 62959B412524DA7800A3D7F1 /* Capacitor.h in Headers */ = {isa = PBXBuildFile; fileRef = 62959B0F2524DA7700A3D7F1 /* Capacitor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 62959B422524DA7800A3D7F1 /* DocLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62959B102524DA7700A3D7F1 /* DocLinks.swift */; }; 62959B432524DA7800A3D7F1 /* Data+Capacitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62959B112524DA7700A3D7F1 /* Data+Capacitor.swift */; }; @@ -62,6 +61,7 @@ 62959BA52526475A00A3D7F1 /* Cordova.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62959BA02526474300A3D7F1 /* Cordova.framework */; }; 6296A77E253A2E49005A202A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6296A77D253A2E49005A202A /* AppDelegate.swift */; }; 6296A782253A2E49005A202A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6296A781253A2E49005A202A /* ViewController.swift */; }; + 6296A7A0253A2E49005A202A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6296A7A1253A2E49005A202A /* SceneDelegate.swift */; }; 6296A785253A2E49005A202A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6296A783253A2E49005A202A /* Main.storyboard */; }; 6296A787253A2E49005A202A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6296A786253A2E49005A202A /* Assets.xcassets */; }; 6296A78A253A2E49005A202A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6296A788253A2E49005A202A /* LaunchScreen.storyboard */; }; @@ -80,6 +80,7 @@ 62FABD1A25AE5C01007B3814 /* Array+Capacitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FABD1925AE5C01007B3814 /* Array+Capacitor.swift */; }; 62FABD2325AE60BA007B3814 /* BridgedTypesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 62FABD2225AE60BA007B3814 /* BridgedTypesTests.m */; }; 62FABD2B25AE6182007B3814 /* BridgedTypesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FABD2A25AE6182007B3814 /* BridgedTypesHelper.swift */; }; + 952707712FD9DD2D0079E5D3 /* CAPSceneDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9527076F2FD9DD260079E5D3 /* CAPSceneDelegateProxy.swift */; }; 957BD9402E78A4A50056874C /* SystemBars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957BD93E2E78A4A20056874C /* SystemBars.swift */; }; A327E6B628DB8B2900CA8B0A /* HttpRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A327E6B228DB8B2800CA8B0A /* HttpRequestHandler.swift */; }; A327E6B728DB8B2900CA8B0A /* CapacitorHttp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A327E6B428DB8B2900CA8B0A /* CapacitorHttp.swift */; }; @@ -204,7 +205,6 @@ 62959B082524DA7700A3D7F1 /* CAPLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAPLog.swift; sourceTree = ""; }; 62959B092524DA7700A3D7F1 /* CAPPluginMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CAPPluginMethod.h; sourceTree = ""; }; 62959B0A2524DA7700A3D7F1 /* CAPBridgeDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAPBridgeDelegate.swift; sourceTree = ""; }; - 62959B0E2524DA7700A3D7F1 /* TmpViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TmpViewController.swift; sourceTree = ""; }; 62959B0F2524DA7700A3D7F1 /* Capacitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Capacitor.h; sourceTree = ""; }; 62959B102524DA7700A3D7F1 /* DocLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocLinks.swift; sourceTree = ""; }; 62959B112524DA7700A3D7F1 /* Data+Capacitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Capacitor.swift"; sourceTree = ""; }; @@ -219,6 +219,7 @@ 6296A77B253A2E49005A202A /* TestsHostApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestsHostApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6296A77D253A2E49005A202A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 6296A781253A2E49005A202A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 6296A7A1253A2E49005A202A /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 6296A784253A2E49005A202A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 6296A786253A2E49005A202A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6296A789253A2E49005A202A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -238,6 +239,7 @@ 62FABD1925AE5C01007B3814 /* Array+Capacitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Capacitor.swift"; sourceTree = ""; }; 62FABD2225AE60BA007B3814 /* BridgedTypesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BridgedTypesTests.m; sourceTree = ""; }; 62FABD2A25AE6182007B3814 /* BridgedTypesHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgedTypesHelper.swift; sourceTree = ""; }; + 9527076F2FD9DD260079E5D3 /* CAPSceneDelegateProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CAPSceneDelegateProxy.swift; sourceTree = ""; }; 957BD93E2E78A4A20056874C /* SystemBars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemBars.swift; sourceTree = ""; }; A327E6B228DB8B2800CA8B0A /* HttpRequestHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpRequestHandler.swift; sourceTree = ""; }; A327E6B428DB8B2900CA8B0A /* CapacitorHttp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapacitorHttp.swift; sourceTree = ""; }; @@ -355,6 +357,7 @@ 62959AE12524DA7700A3D7F1 /* Capacitor */ = { isa = PBXGroup; children = ( + 9527076F2FD9DD260079E5D3 /* CAPSceneDelegateProxy.swift */, A7D9312C2B2370EF00FF59A2 /* Codable */, 0F8F33B127DA980A003F49D6 /* PluginConfig.swift */, A76739782B98E09700795F7B /* PrivacyInfo.xcprivacy */, @@ -395,7 +398,6 @@ 6214934625509C3F006C36F9 /* CAPInstanceConfiguration.swift */, 62959B082524DA7700A3D7F1 /* CAPLog.swift */, 62959AE72524DA7700A3D7F1 /* CAPFile.swift */, - 62959B0E2524DA7700A3D7F1 /* TmpViewController.swift */, 62959B102524DA7700A3D7F1 /* DocLinks.swift */, 62959B152524DA7700A3D7F1 /* CAPNotifications.swift */, 62959B072524DA7700A3D7F1 /* CapacitorExtension.swift */, @@ -446,6 +448,7 @@ isa = PBXGroup; children = ( 6296A77D253A2E49005A202A /* AppDelegate.swift */, + 6296A7A1253A2E49005A202A /* SceneDelegate.swift */, 6296A781253A2E49005A202A /* ViewController.swift */, 62E0735125535E6500BAAADB /* configurations */, 6296A783253A2E49005A202A /* Main.storyboard */, @@ -710,7 +713,6 @@ A7D9312F2B23710300FF59A2 /* JSValueDecoder.swift in Sources */, 62959B362524DA7800A3D7F1 /* CAPBridgeViewController.swift in Sources */, 621ECCB72542045900D3D615 /* CAPBridgedJSTypes.m in Sources */, - 62959B402524DA7800A3D7F1 /* TmpViewController.swift in Sources */, 621ECCD6254205BD00D3D615 /* CAPBridgeProtocol.swift in Sources */, 62D43AF02581817500673C24 /* WKWebView+Capacitor.swift in Sources */, 62959B432524DA7800A3D7F1 /* Data+Capacitor.swift in Sources */, @@ -724,6 +726,7 @@ A7F7EDD5292BE8520015B73B /* CAPInstancePlugin.swift in Sources */, A38C3D7B2848BE6F004B3680 /* CapacitorCookieManager.swift in Sources */, 62959B1B2524DA7800A3D7F1 /* CAPFile.swift in Sources */, + 952707712FD9DD2D0079E5D3 /* CAPSceneDelegateProxy.swift in Sources */, 62959B462524DA7800A3D7F1 /* CAPBridge.swift in Sources */, 62959B1D2524DA7800A3D7F1 /* UIColor.swift in Sources */, 62959B332524DA7800A3D7F1 /* CAPPlugin.m in Sources */, @@ -784,6 +787,7 @@ files = ( 6296A782253A2E49005A202A /* ViewController.swift in Sources */, 6296A77E253A2E49005A202A /* AppDelegate.swift in Sources */, + 6296A7A0253A2E49005A202A /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Capacitor/Capacitor/CAPApplicationDelegateProxy.swift b/ios/Capacitor/Capacitor/CAPApplicationDelegateProxy.swift index 7f510bc5a..ef41457d2 100644 --- a/ios/Capacitor/Capacitor/CAPApplicationDelegateProxy.swift +++ b/ios/Capacitor/Capacitor/CAPApplicationDelegateProxy.swift @@ -4,7 +4,7 @@ import Foundation public class ApplicationDelegateProxy: NSObject, UIApplicationDelegate { public static let shared = ApplicationDelegateProxy() - public private(set) var lastURL: URL? + public internal(set) var lastURL: URL? public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { NotificationCenter.default.post(name: .capacitorOpenURL, object: [ diff --git a/ios/Capacitor/Capacitor/CAPNotifications.swift b/ios/Capacitor/Capacitor/CAPNotifications.swift index 03d80aa87..9731f74e4 100644 --- a/ios/Capacitor/Capacitor/CAPNotifications.swift +++ b/ios/Capacitor/Capacitor/CAPNotifications.swift @@ -18,6 +18,10 @@ extension Notification.Name { public static let capacitorStatusBarTapped = Notification.Name(rawValue: "CapacitorStatusBarTappedNotification") public static let capacitorViewDidAppear = Notification.Name(rawValue: "CapacitorViewDidAppear") public static let capacitorViewWillTransition = Notification.Name(rawValue: "CapacitorViewWillTransition") + public static let capacitorSceneWillConnect = Notification.Name(rawValue: "CapacitorSceneWillConnect") + public static let capacitorSceneOpenURL = Notification.Name(rawValue: "CapacitorSceneOpenURLNotification") + public static let capacitorSceneOpenUniversalLink = + Notification.Name(rawValue: "CapacitorSceneOpenUniversalLinkNotification") } @objc extension NSNotification { @@ -30,6 +34,9 @@ extension Notification.Name { public static let capacitorStatusBarTapped = Notification.Name.capacitorStatusBarTapped public static let capacitorViewDidAppear = Notification.Name.capacitorViewDidAppear public static let capacitorViewWillTransition = Notification.Name.capacitorViewWillTransition + public static let capacitorSceneWillConnect = Notification.Name.capacitorSceneWillConnect + public static let capacitorSceneOpenURL = Notification.Name.capacitorSceneOpenURL + public static let capacitorSceneOpenUniversalLink = Notification.Name.capacitorSceneOpenUniversalLink } /** diff --git a/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift b/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift new file mode 100644 index 000000000..782efe4cf --- /dev/null +++ b/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift @@ -0,0 +1,87 @@ +// +// CAPSceneDelegateProxy.swift +// Capacitor +// +// Created by Joseph Orlando Pender on 6/10/26. +// Copyright © 2026 Drifty Co. All rights reserved. +// + +import Foundation + +@objc(CAPSceneDelegateProxy) +public class SceneDelegateProxy: NSObject, UISceneDelegate { + public static let shared = SceneDelegateProxy() + + public private(set) var lastURL: URL? + + public func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + NotificationCenter.default.post(name: .capacitorSceneWillConnect, object: scene) + + // Plugins haven't loaded yet on a cold start, so notifications posted here are + // missed. Deliver them on the first capacitorViewDidAppear, once plugins are + // registered. + var token: NSObjectProtocol? + token = NotificationCenter.default.addObserver(forName: .capacitorViewDidAppear, object: nil, queue: .main) { _ in + if let token { + NotificationCenter.default.removeObserver(token) + } + if !connectionOptions.urlContexts.isEmpty { + self.scene(scene, openURLContexts: connectionOptions.urlContexts) + } + for userActivity in connectionOptions.userActivities { + self.scene(scene, continue: userActivity) + } + } + } + + public func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + for context in URLContexts { + lastURL = context.url + ApplicationDelegateProxy.shared.lastURL = context.url + let options = Self.openURLOptions(from: context.options) + + // Capacitor 8 backwards compat + NotificationCenter.default.post(name: .capacitorOpenURL, object: [ + "url": context.url, + "options": options + ]) + + NotificationCenter.default.post(name: NSNotification.Name.CDVPluginHandleOpenURL, object: context.url) + + NotificationCenter.default.post(name: .capacitorSceneOpenURL, object: scene, userInfo: [ + "url": context.url, + "options": options + ]) + } + } + + public func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let url = userActivity.webpageURL else { + return + } + lastURL = url + ApplicationDelegateProxy.shared.lastURL = url + + // Capacitor 8 backwards compat + NotificationCenter.default.post(name: .capacitorOpenUniversalLink, object: [ + "url": url + ]) + + NotificationCenter.default.post(name: .capacitorSceneOpenUniversalLink, object: scene, userInfo: [ + "url": url + ]) + } + + private static func openURLOptions(from sceneOptions: UIScene.OpenURLOptions) -> [UIApplication.OpenURLOptionsKey: Any] { + var options: [UIApplication.OpenURLOptionsKey: Any] = [:] + if let sourceApplication = sceneOptions.sourceApplication { + options[.sourceApplication] = sourceApplication + } + if let annotation = sceneOptions.annotation { + options[.annotation] = annotation + } + options[.openInPlace] = sceneOptions.openInPlace + return options + } +} diff --git a/ios/Capacitor/Capacitor/CapacitorBridge.swift b/ios/Capacitor/Capacitor/CapacitorBridge.swift index 7b8b9b99d..263436395 100644 --- a/ios/Capacitor/Capacitor/CapacitorBridge.swift +++ b/ios/Capacitor/Capacitor/CapacitorBridge.swift @@ -93,10 +93,6 @@ open class CapacitorBridge: NSObject, CAPBridgeProtocol { } } } - @available(*, deprecated, message: "obsolete") - var tmpWindow: UIWindow? - @available(*, deprecated, message: "obsolete") - static let tmpVCAppeared = Notification(name: Notification.Name(rawValue: "tmpViewControllerAppeared")) public static let capacitorSite = "https://capacitorjs.com/" public static let fileStartIdentifier = "/_capacitor_file_" public static let httpInterceptorStartIdentifier = "/_capacitor_http_interceptor_" @@ -223,9 +219,6 @@ open class CapacitorBridge: NSObject, CAPBridgeProtocol { setupCordovaCompatibility() exportMiscJS() canInjectJS = false - observers.append(NotificationCenter.default.addObserver(forName: type(of: self).tmpVCAppeared.name, object: .none, queue: .none) { [weak self] _ in - self?.tmpWindow = nil - }) self.setupWebDebugging(configuration: configuration) } @@ -270,11 +263,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 - 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 - 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") + } }) } } diff --git a/ios/Capacitor/Capacitor/TmpViewController.swift b/ios/Capacitor/Capacitor/TmpViewController.swift deleted file mode 100644 index 3511d18ae..000000000 --- a/ios/Capacitor/Capacitor/TmpViewController.swift +++ /dev/null @@ -1,8 +0,0 @@ -import UIKit - -internal class TmpViewController: UIViewController { - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - NotificationCenter.default.post(CapacitorBridge.tmpVCAppeared) - } -} diff --git a/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift b/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift index 2b4a256d0..866c7e8c5 100644 --- a/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift +++ b/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift @@ -107,7 +107,7 @@ open class WebViewDelegationHandler: NSObject, WKNavigationDelegate, WKUIDelegat if !isApplicationNavigation, toplevelNavigation { // disallow and let the system handle it - if UIApplication.shared.applicationState == .active { + if webView.window?.windowScene?.activationState == .foregroundActive { UIApplication.shared.open(navURL, options: [:], completionHandler: nil) } decisionHandler(.cancel) diff --git a/ios/Capacitor/TestsHostApp/AppDelegate.swift b/ios/Capacitor/TestsHostApp/AppDelegate.swift index f8b87fe2d..9107e3c45 100644 --- a/ios/Capacitor/TestsHostApp/AppDelegate.swift +++ b/ios/Capacitor/TestsHostApp/AppDelegate.swift @@ -6,4 +6,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. return true } + + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + let config = UISceneConfiguration(name: "Default Configuration", + sessionRole: connectingSceneSession.role) + config.delegateClass = SceneDelegate.self + return config + } } diff --git a/ios/Capacitor/TestsHostApp/SceneDelegate.swift b/ios/Capacitor/TestsHostApp/SceneDelegate.swift new file mode 100644 index 000000000..e0be6e9cf --- /dev/null +++ b/ios/Capacitor/TestsHostApp/SceneDelegate.swift @@ -0,0 +1,5 @@ +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? +} From 984fa85ba0adab0aacf895aed6323bf4b503dccb Mon Sep 17 00:00:00 2001 From: Joey Pender Date: Fri, 31 Jul 2026 09:29:58 -0500 Subject: [PATCH 27/30] feat(cli): add migrator functionality for adopting UIScene (#8544) --- cli/package.json | 2 + cli/src/tasks/migrate-uiscene.ts | 305 ++++++++++++++++++ cli/src/tasks/migrate.ts | 10 + cli/src/util/spm.ts | 42 +++ cli/src/util/xcode.ts | 53 +++ cli/test/migrate-uiscene-detect.spec.ts | 36 +++ cli/test/migrate-uiscene-plist.spec.ts | 159 +++++++++ cli/test/migrate-uiscene-scan.spec.ts | 167 ++++++++++ .../migrate-uiscene-scene-delegate.spec.ts | 98 ++++++ cli/test/xcode.spec.ts | 114 +++++++ 10 files changed, 986 insertions(+) create mode 100644 cli/src/tasks/migrate-uiscene.ts create mode 100644 cli/src/util/xcode.ts create mode 100644 cli/test/migrate-uiscene-detect.spec.ts create mode 100644 cli/test/migrate-uiscene-plist.spec.ts create mode 100644 cli/test/migrate-uiscene-scan.spec.ts create mode 100644 cli/test/migrate-uiscene-scene-delegate.spec.ts create mode 100644 cli/test/xcode.spec.ts diff --git a/cli/package.json b/cli/package.json index e0c99f6d0..627f846a3 100644 --- a/cli/package.json +++ b/cli/package.json @@ -60,6 +60,7 @@ "semver": "^7.6.3", "tar": "^7.5.3", "tslib": "^2.8.1", + "xcode": "^3.0.1", "xml2js": "^0.6.2" }, "devDependencies": { @@ -70,6 +71,7 @@ "@types/prompts": "^2.4.9", "@types/semver": "^7.5.8", "@types/tmp": "^0.2.6", + "@types/xcode": "^3.0.0", "@types/xml2js": "0.4.5", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", diff --git a/cli/src/tasks/migrate-uiscene.ts b/cli/src/tasks/migrate-uiscene.ts new file mode 100644 index 000000000..9f6dfa278 --- /dev/null +++ b/cli/src/tasks/migrate-uiscene.ts @@ -0,0 +1,305 @@ +import { existsSync, readFileSync, writeFileSync } from 'fs-extra'; +import { join, sep } from 'path'; + +import { runTask } from '../common'; +import type { Config } from '../definitions'; +import { logger } from '../log'; +import { deleteFolderRecursive, readdirp } from '../util/fs'; +import { addSceneManifestIfNeeded, hasSceneManifest } from '../util/spm'; +import { extractTemplate } from '../util/template'; +import { addSwiftFileToAppTarget } from '../util/xcode'; + +type PreUISceneState = 'eligible' | 'already-migrated' | 'partial'; + +interface UISceneDetectionSignals { + hasManifest: boolean; + hasSceneDelegate: boolean; + hasConfigurationForConnecting: boolean; +} + +interface TemplateAssets { + sceneDelegate: string; + configurationForConnectingSnippet: string; +} + +export async function migrateToUIScene(config: Config): Promise { + const signals = readDetectionSignals(config); + const state = classify(signals); + + switch (state) { + case 'already-migrated': + logger.info('UIScene migration: project already migrated, skipping.'); + return; + case 'partial': + logger.warn( + `UIScene migration: project is in a partial state (${describeSignals(signals)}). ` + + `Skipping automated migration — finish the migration by hand or reset to a clean 8.4 state first.`, + ); + return; + case 'eligible': + break; + } + + const assets = await loadTemplateAssets(config); + if (!assets) { + logger.error('UIScene migration: could not read shipped iOS template assets; skipping.'); + return; + } + + await runTask('Adding UIApplicationSceneManifest to Info.plist.', () => addSceneManifestIfNeeded(config)); + + await runTask('Writing SceneDelegate.swift.', async () => { + const { written } = writeSceneDelegate(config, assets.sceneDelegate); + if (!written) { + logger.warn('SceneDelegate.swift already exists, skipping.'); + } + }); + + await runTask('Patching AppDelegate.swift with configurationForConnecting.', async () => { + const { patched, reason } = patchAppDelegate(config, assets.configurationForConnectingSnippet); + if (!patched && reason) { + logger.warn(`AppDelegate.swift not patched: ${reason}`); + } + }); + + await runTask('Registering SceneDelegate.swift with the Xcode App target.', async () => { + const pbxprojPath = join(config.ios.nativeXcodeProjDirAbs, 'project.pbxproj'); + try { + const { added } = addSwiftFileToAppTarget(pbxprojPath, 'App', 'SceneDelegate.swift'); + if (!added) { + logger.warn('SceneDelegate.swift is already registered in the App target, skipping.'); + } + } catch (err: any) { + logger.warn( + `Could not register SceneDelegate.swift automatically: ${err?.message ?? err}. ` + + 'Add SceneDelegate.swift to the App target in Xcode manually.', + ); + } + }); + + await scanAndWarn(config); + printNextSteps(); +} + +async function scanAndWarn(config: Config): Promise { + const findings: string[] = []; + + const swiftFiles = await readdirp(config.ios.platformDirAbs, { + filter: (item) => { + if (!item.stats.isFile()) return false; + if (!item.path.endsWith('.swift')) return false; + const p = item.path; + return ( + !p.includes(`${sep}Pods${sep}`) && + !p.includes(`${sep}build${sep}`) && + !p.includes(`${sep}DerivedData${sep}`) && + !p.includes(`${sep}.build${sep}`) + ); + }, + }); + + const tokenPatterns: { token: RegExp; label: string }[] = [ + { token: /UIApplication\.shared\.applicationState/, label: 'UIApplication.shared.applicationState' }, + { token: /\btmpWindow\b/, label: 'tmpWindow' }, + { token: /\bTmpViewController\b/, label: 'TmpViewController' }, + ]; + + for (const filePath of swiftFiles) { + const source = readFileSync(filePath, 'utf-8'); + source.split('\n').forEach((line, idx) => { + for (const { token, label } of tokenPatterns) { + if (token.test(line)) { + findings.push(`${filePath}:${idx + 1}: uses ${label}`); + } + } + }); + } + + const appDelegatePath = join(config.ios.nativeTargetDirAbs, 'AppDelegate.swift'); + if (existsSync(appDelegatePath)) { + const source = readFileSync(appDelegatePath, 'utf-8'); + if (hasCustomDelegateBody(source, /func application\([^)]*\bopen url:/)) { + findings.push(`${appDelegatePath}: custom application(_:open:) body — review for UIScene compatibility`); + } + if (hasCustomDelegateBody(source, /func application\([^)]*\bcontinue userActivity:/)) { + findings.push(`${appDelegatePath}: custom application(_:continue:) body — review for UIScene compatibility`); + } + } + + if (findings.length === 0) return; + + logger.warn('UIScene scan found patterns that may need manual review:'); + for (const finding of findings) { + logger.warn(` ${finding}`); + } +} + +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; + while (i < source.length && depth > 0) { + const ch = source[i]; + if (ch === '{') depth++; + else if (ch === '}') depth--; + i++; + } + if (depth !== 0) return false; + const body = source.slice(openIdx + 1, i - 1); + const codeLines = body + .split('\n') + .map((l) => l.trim()) + .filter((l) => l.length > 0 && !l.startsWith('//')); + if (codeLines.length === 0) return false; + return codeLines.some((l) => !l.includes('ApplicationDelegateProxy.shared')); +} + +function printNextSteps(): void { + logger.info(''); + logger.info('UIScene migration next steps:'); + logger.info(' • Review any warnings above for legacy API usage or custom AppDelegate URL/activity handlers.'); + logger.info(' • Full guide: https://capacitorjs.com/docs/updating/8-5'); +} + +async function loadTemplateAssets(config: Config): Promise { + const packageManager = await config.ios.packageManager; + const archiveName = packageManager === 'SPM' ? 'ios-spm-template.tar.gz' : 'ios-pods-template.tar.gz'; + const archivePath = join(config.cli.assetsDirAbs, archiveName); + const tempDir = join(config.cli.assetsDirAbs, 'tempUISceneTemplate'); + + try { + await extractTemplate(archivePath, tempDir); + const sceneDelegatePath = join(tempDir, 'App', 'App', 'SceneDelegate.swift'); + const appDelegatePath = join(tempDir, 'App', 'App', 'AppDelegate.swift'); + if (!existsSync(sceneDelegatePath) || !existsSync(appDelegatePath)) { + return null; + } + const sceneDelegate = readFileSync(sceneDelegatePath, 'utf-8'); + const appDelegateSource = readFileSync(appDelegatePath, 'utf-8'); + const configurationForConnectingSnippet = extractConfigurationForConnecting(appDelegateSource); + if (!configurationForConnectingSnippet) { + return null; + } + return { sceneDelegate, configurationForConnectingSnippet }; + } finally { + deleteFolderRecursive(tempDir); + } +} + +function writeSceneDelegate(config: Config, contents: string): { written: boolean } { + const path = join(config.ios.nativeTargetDirAbs, 'SceneDelegate.swift'); + if (existsSync(path)) { + return { written: false }; + } + writeFileSync(path, contents); + return { written: true }; +} + +function patchAppDelegate(config: Config, snippet: string): { patched: boolean; reason?: string } { + const path = join(config.ios.nativeTargetDirAbs, 'AppDelegate.swift'); + if (!existsSync(path)) { + return { patched: false, reason: 'AppDelegate.swift not found.' }; + } + const source = readFileSync(path, 'utf-8'); + if (source.includes('UISceneConfiguration(name:')) { + return { patched: false, reason: 'configurationForConnecting already present.' }; + } + const patched = insertBeforeAppDelegateClassEnd(source, snippet); + if (!patched) { + return { patched: false, reason: 'could not locate AppDelegate class body.' }; + } + writeFileSync(path, patched); + return { patched: true }; +} + +function extractConfigurationForConnecting(appDelegateSource: string): string | null { + const sigRegex = /^ {4}func application\(_ application: UIApplication,\n {21}configurationForConnecting\b/m; + const sigMatch = appDelegateSource.match(sigRegex); + if (!sigMatch || sigMatch.index === undefined) { + return null; + } + const openIdx = appDelegateSource.indexOf('{', sigMatch.index); + if (openIdx === -1) { + return 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, i) + '\n'; +} + +function insertBeforeAppDelegateClassEnd(source: string, snippet: string): string | null { + const classDeclRegex = /\bclass\s+AppDelegate\b[^{]*\{/; + const match = source.match(classDeclRegex); + if (!match || match.index === undefined) { + return null; + } + const openIdx = source.indexOf('{', match.index); + 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); +} + +function readDetectionSignals(config: Config): UISceneDetectionSignals { + const sceneDelegatePath = join(config.ios.nativeTargetDirAbs, 'SceneDelegate.swift'); + const appDelegatePath = join(config.ios.nativeTargetDirAbs, 'AppDelegate.swift'); + + return { + hasManifest: hasSceneManifest(config), + hasSceneDelegate: existsSync(sceneDelegatePath), + hasConfigurationForConnecting: + existsSync(appDelegatePath) && readFileSync(appDelegatePath, 'utf-8').includes('UISceneConfiguration(name:'), + }; +} + +function classify(signals: UISceneDetectionSignals): PreUISceneState { + const { hasManifest, hasSceneDelegate, hasConfigurationForConnecting } = signals; + const trueCount = [hasManifest, hasSceneDelegate, hasConfigurationForConnecting].filter(Boolean).length; + if (trueCount === 0) return 'eligible'; + if (trueCount === 3) return 'already-migrated'; + return 'partial'; +} + +function describeSignals({ + hasManifest, + hasSceneDelegate, + hasConfigurationForConnecting, +}: UISceneDetectionSignals): string { + const present: string[] = []; + const missing: string[] = []; + (hasManifest ? present : missing).push('UIApplicationSceneManifest'); + (hasSceneDelegate ? present : missing).push('SceneDelegate.swift'); + (hasConfigurationForConnecting ? present : missing).push('AppDelegate.configurationForConnecting'); + return `present: [${present.join(', ')}]; missing: [${missing.join(', ')}]`; +} + +// Exported for tests. +export const __testables = { + classify, + describeSignals, + insertBeforeAppDelegateClassEnd, + extractConfigurationForConnecting, + hasCustomDelegateBody, + scanAndWarn, +}; diff --git a/cli/src/tasks/migrate.ts b/cli/src/tasks/migrate.ts index 8524ad733..eec9673d5 100644 --- a/cli/src/tasks/migrate.ts +++ b/cli/src/tasks/migrate.ts @@ -13,6 +13,8 @@ import { deleteFolderRecursive } from '../util/fs'; import { runCommand } from '../util/subprocess'; import { extractTemplate } from '../util/template'; +import { migrateToUIScene } from './migrate-uiscene'; + // eslint-disable-next-line prefer-const let allDependencies: { [key: string]: any } = {}; const libs = ['@capacitor/core', '@capacitor/cli', '@capacitor/ios', '@capacitor/android']; @@ -175,6 +177,8 @@ export async function migrateCommand(config: Config, noprompt: boolean, packagem } else { logger.warn('Skipped updating deployment target'); } + + await migrateToUIScene(config); } if (!installFailed) { @@ -391,6 +395,12 @@ async function writeBreakingChanges() { )}.`, ); } + 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.', + ); + } } async function getAndroidVariablesAndClasspaths(config: Config) { diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts index 2ef26bcda..65cd0f11f 100644 --- a/cli/src/util/spm.ts +++ b/cli/src/util/spm.ts @@ -239,6 +239,48 @@ export async function addInfoPlistDebugIfNeeded(config: Config): Promise { } } +export function hasSceneManifest(config: Config): boolean { + const infoPlist = resolve(config.ios.nativeTargetDirAbs, 'Info.plist'); + if (!existsSync(infoPlist)) { + return false; + } + const entries = parse(readFileSync(infoPlist, 'utf-8')) as PlistObject; + return entries['UIApplicationSceneManifest'] !== undefined; +} + +export async function addSceneManifestIfNeeded(config: Config): Promise { + type Mutable = { -readonly [P in keyof T]: T[P] }; + + const infoPlist = resolve(config.ios.nativeTargetDirAbs, 'Info.plist'); + + if (!existsSync(infoPlist)) { + logger.warn(infoPlist + ' not found.'); + return; + } + + const entries = parse(readFileSync(infoPlist, 'utf-8')) as Mutable; + + if (entries['UIApplicationSceneManifest'] !== undefined) { + logger.warn('Found UIApplicationSceneManifest in ' + infoPlist + ', skipping.'); + return; + } + + entries['UIApplicationSceneManifest'] = { + UIApplicationSupportsMultipleScenes: false, + UISceneConfigurations: { + UIWindowSceneSessionRoleApplication: [ + { + UISceneConfigurationName: 'Default Configuration', + UISceneDelegateClassName: '$(PRODUCT_MODULE_NAME).SceneDelegate', + UISceneStoryboardFile: 'Main', + }, + ], + }, + }; + + writeFileSync(infoPlist, build(entries)); +} + export async function checkSwiftToolsVersion(config: Config, version: string | undefined): Promise { if (!version) { return null; diff --git a/cli/src/util/xcode.ts b/cli/src/util/xcode.ts new file mode 100644 index 000000000..64ab836a3 --- /dev/null +++ b/cli/src/util/xcode.ts @@ -0,0 +1,53 @@ +import { writeFileSync } from 'fs-extra'; +import { project as loadXcodeProject } from 'xcode'; +import type { XcodeProject } from 'xcode'; + +/** + * Register a Swift source file with the first native target of an Xcode project. + * + * Writes entries in PBXFileReference, PBXBuildFile, the named PBXGroup's children, + * and the target's Sources build phase. Idempotent: a second call is a no-op. + * + * @param pbxprojPath Absolute path to project.pbxproj + * @param groupName Comment/name of the PBXGroup the file belongs under (e.g. 'App') + * @param fileRelPath Path of the file relative to the group (e.g. 'SceneDelegate.swift') + */ +export function addSwiftFileToAppTarget( + pbxprojPath: string, + groupName: string, + fileRelPath: string, +): { added: boolean } { + const project = loadXcodeProject(pbxprojPath); + project.parseSync(); + + if (project.hasFile(fileRelPath)) { + return { added: false }; + } + + const groupUuid = findGroupUuidByComment(project, groupName); + if (!groupUuid) { + throw new Error(`Could not find PBXGroup with comment "${groupName}" in ${pbxprojPath}`); + } + + const targetUuid = project.getFirstTarget().uuid; + const result = project.addSourceFile(fileRelPath, { target: targetUuid }, groupUuid); + if (!result) { + throw new Error(`Failed to register ${fileRelPath} in ${pbxprojPath}`); + } + + writeFileSync(pbxprojPath, project.writeSync(), 'utf-8'); + return { added: true }; +} + +// Exported for tests. +export function findGroupUuidByComment(project: XcodeProject, comment: string): string | null { + const groups = project.hash.project.objects.PBXGroup; + const COMMENT_SUFFIX = '_comment'; + for (const key of Object.keys(groups)) { + if (!key.endsWith(COMMENT_SUFFIX)) continue; + if (groups[key] === comment) { + return key.substring(0, key.length - COMMENT_SUFFIX.length); + } + } + return null; +} diff --git a/cli/test/migrate-uiscene-detect.spec.ts b/cli/test/migrate-uiscene-detect.spec.ts new file mode 100644 index 000000000..4c73819e2 --- /dev/null +++ b/cli/test/migrate-uiscene-detect.spec.ts @@ -0,0 +1,36 @@ +import { __testables } from '../src/tasks/migrate-uiscene'; + +const { classify } = __testables; + +describe('migrate-uiscene classify', () => { + it('returns eligible when nothing scene-related is present', () => { + expect( + classify({ + hasManifest: false, + hasSceneDelegate: false, + hasConfigurationForConnecting: false, + }), + ).toBe('eligible'); + }); + + it('returns already-migrated when all three signals are present', () => { + expect( + classify({ + hasManifest: true, + hasSceneDelegate: true, + hasConfigurationForConnecting: true, + }), + ).toBe('already-migrated'); + }); + + it.each([ + [{ hasManifest: true, hasSceneDelegate: false, hasConfigurationForConnecting: false }], + [{ hasManifest: false, hasSceneDelegate: true, hasConfigurationForConnecting: false }], + [{ hasManifest: false, hasSceneDelegate: false, hasConfigurationForConnecting: true }], + [{ hasManifest: true, hasSceneDelegate: true, hasConfigurationForConnecting: false }], + [{ hasManifest: true, hasSceneDelegate: false, hasConfigurationForConnecting: true }], + [{ hasManifest: false, hasSceneDelegate: true, hasConfigurationForConnecting: true }], + ])('returns partial for mixed signals %j', (signals) => { + expect(classify(signals)).toBe('partial'); + }); +}); diff --git a/cli/test/migrate-uiscene-plist.spec.ts b/cli/test/migrate-uiscene-plist.spec.ts new file mode 100644 index 000000000..dc3bd2513 --- /dev/null +++ b/cli/test/migrate-uiscene-plist.spec.ts @@ -0,0 +1,159 @@ +import { readFileSync, writeFileSync } from 'fs-extra'; +import { join } from 'path'; +import type { PlistObject } from 'plist'; +import { parse } from 'plist'; + +import type { Config } from '../src/definitions'; +import { addSceneManifestIfNeeded, hasSceneManifest } from '../src/util/spm'; + +import { mktmp } from './util'; + +const PRE_UISCENE_INFO_PLIST = ` + + + + CFBundleDisplayName + My App + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + + +`; + +const POST_UISCENE_INFO_PLIST = ` + + + + CFBundleDisplayName + My App + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Existing Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).CustomSceneDelegate + + + + + + +`; + +function makeFakeConfig(nativeTargetDirAbs: string): Config { + return { ios: { nativeTargetDirAbs } } as unknown as Config; +} + +describe('addSceneManifestIfNeeded', () => { + let tmpDir: any; + let plistPath: string; + let config: Config; + + beforeEach(async () => { + tmpDir = await mktmp(); + plistPath = join(tmpDir.path, 'Info.plist'); + config = makeFakeConfig(tmpDir.path); + }); + + afterEach(() => { + tmpDir.cleanupCallback(); + }); + + it('adds the scene manifest when absent', async () => { + writeFileSync(plistPath, PRE_UISCENE_INFO_PLIST); + + await addSceneManifestIfNeeded(config); + + const parsed = parse(readFileSync(plistPath, 'utf-8')) as PlistObject; + const manifest = parsed['UIApplicationSceneManifest'] as PlistObject; + expect(manifest['UIApplicationSupportsMultipleScenes']).toBe(false); + const configs = manifest['UISceneConfigurations'] as PlistObject; + const roleArray = configs['UIWindowSceneSessionRoleApplication'] as PlistObject[]; + expect(roleArray).toHaveLength(1); + expect(roleArray[0]['UISceneConfigurationName']).toBe('Default Configuration'); + expect(roleArray[0]['UISceneDelegateClassName']).toBe('$(PRODUCT_MODULE_NAME).SceneDelegate'); + expect(roleArray[0]['UISceneStoryboardFile']).toBe('Main'); + }); + + it('preserves sibling keys', async () => { + writeFileSync(plistPath, PRE_UISCENE_INFO_PLIST); + + await addSceneManifestIfNeeded(config); + + const parsed = parse(readFileSync(plistPath, 'utf-8')) as PlistObject; + expect(parsed['CFBundleDisplayName']).toBe('My App'); + expect(parsed['UILaunchStoryboardName']).toBe('LaunchScreen'); + expect(parsed['UIMainStoryboardFile']).toBe('Main'); + }); + + it('is a no-op when the manifest already exists and does not overwrite user data', async () => { + writeFileSync(plistPath, POST_UISCENE_INFO_PLIST); + + await addSceneManifestIfNeeded(config); + + const parsed = parse(readFileSync(plistPath, 'utf-8')) as PlistObject; + const manifest = parsed['UIApplicationSceneManifest'] as PlistObject; + const configs = manifest['UISceneConfigurations'] as PlistObject; + const roleArray = configs['UIWindowSceneSessionRoleApplication'] as PlistObject[]; + expect(roleArray[0]['UISceneConfigurationName']).toBe('Existing Configuration'); + expect(roleArray[0]['UISceneDelegateClassName']).toBe('$(PRODUCT_MODULE_NAME).CustomSceneDelegate'); + }); + + it('is idempotent across repeated runs', async () => { + writeFileSync(plistPath, PRE_UISCENE_INFO_PLIST); + + await addSceneManifestIfNeeded(config); + const firstOutput = readFileSync(plistPath, 'utf-8'); + await addSceneManifestIfNeeded(config); + const secondOutput = readFileSync(plistPath, 'utf-8'); + await addSceneManifestIfNeeded(config); + const thirdOutput = readFileSync(plistPath, 'utf-8'); + + expect(secondOutput).toBe(firstOutput); + expect(thirdOutput).toBe(firstOutput); + }); + + it('is a no-op when the plist does not exist', async () => { + // No write — plist file is missing. + await expect(addSceneManifestIfNeeded(config)).resolves.toBeUndefined(); + }); +}); + +describe('hasSceneManifest', () => { + let tmpDir: any; + let plistPath: string; + let config: Config; + + beforeEach(async () => { + tmpDir = await mktmp(); + plistPath = join(tmpDir.path, 'Info.plist'); + config = makeFakeConfig(tmpDir.path); + }); + + afterEach(() => { + tmpDir.cleanupCallback(); + }); + + it('returns false when the manifest key is absent', () => { + writeFileSync(plistPath, PRE_UISCENE_INFO_PLIST); + expect(hasSceneManifest(config)).toBe(false); + }); + + it('returns true when the manifest key is present', () => { + writeFileSync(plistPath, POST_UISCENE_INFO_PLIST); + expect(hasSceneManifest(config)).toBe(true); + }); + + it('returns false when the plist does not exist', () => { + expect(hasSceneManifest(config)).toBe(false); + }); +}); diff --git a/cli/test/migrate-uiscene-scan.spec.ts b/cli/test/migrate-uiscene-scan.spec.ts new file mode 100644 index 000000000..c0d7964dd --- /dev/null +++ b/cli/test/migrate-uiscene-scan.spec.ts @@ -0,0 +1,167 @@ +import { mkdirp, writeFileSync } from 'fs-extra'; +import { join } from 'path'; + +import type { Config } from '../src/definitions'; +import { logger } from '../src/log'; +import { __testables } from '../src/tasks/migrate-uiscene'; + +import { mktmp } from './util'; + +const { hasCustomDelegateBody, scanAndWarn } = __testables; + +const OPEN_URL_SIG = /func application\([^)]*\bopen url:/; +const CONTINUE_SIG = /func application\([^)]*\bcontinue userActivity:/; + +const VANILLA_OPEN_URL = ` +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Called when the app was launched with a url. + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } +} +`; + +const CUSTOM_OPEN_URL = ` +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + print("received url: \\(url)") + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } +} +`; + +const VANILLA_CONTINUE = ` +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) + } +} +`; + +const CUSTOM_CONTINUE = ` +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + MyAnalytics.track(userActivity) + return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) + } +} +`; + +describe('hasCustomDelegateBody', () => { + it('returns false for the vanilla open-url body', () => { + expect(hasCustomDelegateBody(VANILLA_OPEN_URL, OPEN_URL_SIG)).toBe(false); + }); + + it('returns true for an open-url body with a print statement', () => { + expect(hasCustomDelegateBody(CUSTOM_OPEN_URL, OPEN_URL_SIG)).toBe(true); + }); + + it('returns false for the vanilla continue userActivity body', () => { + expect(hasCustomDelegateBody(VANILLA_CONTINUE, CONTINUE_SIG)).toBe(false); + }); + + it('returns true for a continue userActivity body with extra work', () => { + expect(hasCustomDelegateBody(CUSTOM_CONTINUE, CONTINUE_SIG)).toBe(true); + }); + + it('returns false when the method is not present', () => { + expect(hasCustomDelegateBody('class AppDelegate {}', OPEN_URL_SIG)).toBe(false); + }); + + it('ignores line comments inside the body', () => { + const source = ` +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // This comment is fine, feel free to add processing. + // Second comment line. + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } +} +`; + expect(hasCustomDelegateBody(source, OPEN_URL_SIG)).toBe(false); + }); +}); + +describe('scanAndWarn', () => { + let tmpDir: any; + let iosDir: string; + let appDir: string; + let warnSpy: jest.SpyInstance; + let infoSpy: jest.SpyInstance; + + beforeEach(async () => { + tmpDir = await mktmp(); + iosDir = join(tmpDir.path, 'ios'); + appDir = join(iosDir, 'App', 'App'); + await mkdirp(appDir); + warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined); + infoSpy = jest.spyOn(logger, 'info').mockImplementation(() => undefined); + }); + + afterEach(() => { + warnSpy.mockRestore(); + infoSpy.mockRestore(); + tmpDir.cleanupCallback(); + }); + + function makeConfig(): Config { + return { + ios: { platformDirAbs: iosDir, nativeTargetDirAbs: appDir }, + } as unknown as Config; + } + + it('warns on UIApplication.shared.applicationState references', async () => { + writeFileSync( + join(appDir, 'SomeVC.swift'), + `import UIKit\nclass Foo {\n func check() {\n if UIApplication.shared.applicationState == .active { print("hi") }\n }\n}\n`, + ); + + await scanAndWarn(makeConfig()); + + const messages = warnSpy.mock.calls.map((c) => c[0]); + expect(messages.some((m) => m.includes('applicationState'))).toBe(true); + expect(messages.some((m) => m.includes('SomeVC.swift:4'))).toBe(true); + }); + + it('warns on tmpWindow and TmpViewController references', async () => { + writeFileSync(join(appDir, 'Legacy.swift'), `let w = tmpWindow\nlet vc: TmpViewController? = nil\n`); + + await scanAndWarn(makeConfig()); + + const messages = warnSpy.mock.calls.map((c) => c[0]); + expect(messages.some((m) => m.includes('tmpWindow'))).toBe(true); + expect(messages.some((m) => m.includes('TmpViewController'))).toBe(true); + }); + + it('warns on custom AppDelegate handlers', async () => { + writeFileSync(join(appDir, 'AppDelegate.swift'), CUSTOM_OPEN_URL + CUSTOM_CONTINUE); + + await scanAndWarn(makeConfig()); + + const messages = warnSpy.mock.calls.map((c) => c[0]); + expect(messages.some((m) => m.includes('custom application(_:open:) body'))).toBe(true); + expect(messages.some((m) => m.includes('custom application(_:continue:) body'))).toBe(true); + }); + + it('is silent on a clean project', async () => { + writeFileSync(join(appDir, 'AppDelegate.swift'), VANILLA_OPEN_URL + VANILLA_CONTINUE); + writeFileSync(join(appDir, 'CleanCode.swift'), `class Clean {}\n`); + + await scanAndWarn(makeConfig()); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('skips Pods/, build/, and DerivedData/ directories', async () => { + const podsDir = join(iosDir, 'App', 'Pods'); + const buildDir = join(iosDir, 'App', 'build'); + await mkdirp(podsDir); + await mkdirp(buildDir); + writeFileSync(join(podsDir, 'ThirdParty.swift'), `let x = UIApplication.shared.applicationState\n`); + writeFileSync(join(buildDir, 'Generated.swift'), `class TmpViewController {}\n`); + + await scanAndWarn(makeConfig()); + + expect(warnSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/cli/test/migrate-uiscene-scene-delegate.spec.ts b/cli/test/migrate-uiscene-scene-delegate.spec.ts new file mode 100644 index 000000000..b2f400588 --- /dev/null +++ b/cli/test/migrate-uiscene-scene-delegate.spec.ts @@ -0,0 +1,98 @@ +import { readFileSync } from 'fs-extra'; +import { resolve } from 'path'; + +import { __testables } from '../src/tasks/migrate-uiscene'; + +const { extractConfigurationForConnecting, insertBeforeAppDelegateClassEnd } = __testables; + +const REPO_ROOT = resolve(__dirname, '..', '..'); +const SHIPPED_APP_DELEGATE_SPM = resolve(REPO_ROOT, 'ios-spm-template/App/App/AppDelegate.swift'); +const SHIPPED_APP_DELEGATE_PODS = resolve(REPO_ROOT, 'ios-pods-template/App/App/AppDelegate.swift'); + +describe('extractConfigurationForConnecting', () => { + it('slices the method out of the shipped SPM AppDelegate', () => { + const shipped = readFileSync(SHIPPED_APP_DELEGATE_SPM, 'utf-8'); + const snippet = extractConfigurationForConnecting(shipped); + expect(snippet).not.toBeNull(); + expect(snippet).toContain('configurationForConnecting connectingSceneSession: UISceneSession'); + expect(snippet).toContain('UISceneConfiguration(name: "Default Configuration"'); + expect(snippet).toContain('config.delegateClass = SceneDelegate.self'); + expect(snippet!.startsWith('\n')).toBe(true); + expect(snippet!.endsWith('\n')).toBe(true); + }); + + it('slices the method out of the shipped Pods AppDelegate', () => { + const shipped = readFileSync(SHIPPED_APP_DELEGATE_PODS, 'utf-8'); + const snippet = extractConfigurationForConnecting(shipped); + expect(snippet).not.toBeNull(); + expect(snippet).toContain('configurationForConnecting connectingSceneSession: UISceneSession'); + expect(snippet).toContain('config.delegateClass = SceneDelegate.self'); + }); + + it('returns null when the method signature is absent', () => { + const source = `import UIKit\n\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n var window: UIWindow?\n}\n`; + expect(extractConfigurationForConnecting(source)).toBeNull(); + }); +}); + +describe('insertBeforeAppDelegateClassEnd', () => { + it('turns a pre-UIScene AppDelegate into the shipped SPM AppDelegate', () => { + const shipped = readFileSync(SHIPPED_APP_DELEGATE_SPM, 'utf-8'); + const snippet = extractConfigurationForConnecting(shipped); + expect(snippet).not.toBeNull(); + const pre = stripConfigurationForConnecting(shipped); + + const patched = insertBeforeAppDelegateClassEnd(pre, snippet!); + + expect(patched).toBe(shipped); + }); + + it('returns null when no AppDelegate class is present', () => { + const source = `import UIKit\n\nstruct Something {}\n`; + expect(insertBeforeAppDelegateClassEnd(source, '\n func x() {}\n')).toBeNull(); + }); + + it('returns null when class braces are unbalanced', () => { + const source = `class AppDelegate: UIResponder {\n func foo() {\n`; + expect(insertBeforeAppDelegateClassEnd(source, '\n func x() {}\n')).toBeNull(); + }); + + it('inserts inside the class, before its closing brace, even with a trailing extension', () => { + const source = [ + 'class AppDelegate: UIResponder, UIApplicationDelegate {', + ' func a() {}', + '}', + '', + 'extension AppDelegate {', + ' func b() {}', + '}', + '', + ].join('\n'); + + const patched = insertBeforeAppDelegateClassEnd(source, '\n func injected() {}\n'); + + expect(patched).toBe( + [ + 'class AppDelegate: UIResponder, UIApplicationDelegate {', + ' func a() {}', + '', + ' func injected() {}', + '}', + '', + 'extension AppDelegate {', + ' func b() {}', + '}', + '', + ].join('\n'), + ); + }); +}); + +function stripConfigurationForConnecting(source: string): string { + // Remove the configurationForConnecting method + its leading blank line from the shipped template, + // yielding an 8.4-shaped AppDelegate for use as a fixture. + return source.replace( + /\n\n {4}func application\(_ application: UIApplication,\n {21}configurationForConnecting[\s\S]*?\n {4}\}\n\}/, + '\n}', + ); +} diff --git a/cli/test/xcode.spec.ts b/cli/test/xcode.spec.ts new file mode 100644 index 000000000..e6aa08357 --- /dev/null +++ b/cli/test/xcode.spec.ts @@ -0,0 +1,114 @@ +import { readFileSync, writeFileSync } from 'fs-extra'; +import { join, resolve } from 'path'; +import { project as loadXcodeProject } from 'xcode'; + +import { addSwiftFileToAppTarget, findGroupUuidByComment } from '../src/util/xcode'; + +import { mktmp } from './util'; + +const REPO_ROOT = resolve(__dirname, '..', '..'); +const SHIPPED_PBXPROJ = resolve(REPO_ROOT, 'ios-spm-template/App/App.xcodeproj/project.pbxproj'); + +function stripSceneDelegate(source: string): string { + return source + .split('\n') + .filter((line) => !line.includes('SceneDelegate')) + .join('\n'); +} + +describe('findGroupUuidByComment', () => { + it('returns the UUID of the App group in the shipped pbxproj', () => { + const project = loadXcodeProject(SHIPPED_PBXPROJ); + project.parseSync(); + + const uuid = findGroupUuidByComment(project, 'App'); + + expect(uuid).toMatch(/^[A-F0-9]{24}$/); + const group = project.getPBXGroupByKey(uuid!); + expect(group).toBeDefined(); + expect(group?.path).toBe('App'); + }); + + it('returns null for a group name that does not exist', () => { + const project = loadXcodeProject(SHIPPED_PBXPROJ); + project.parseSync(); + + expect(findGroupUuidByComment(project, 'NonExistentGroup')).toBeNull(); + }); +}); + +describe('addSwiftFileToAppTarget', () => { + let tmpDir: any; + let pbxprojPath: string; + + beforeEach(async () => { + tmpDir = await mktmp(); + pbxprojPath = join(tmpDir.path, 'project.pbxproj'); + }); + + afterEach(() => { + tmpDir.cleanupCallback(); + }); + + it('registers a new Swift file in all four pbxproj sections', () => { + const preUISceneSource = stripSceneDelegate(readFileSync(SHIPPED_PBXPROJ, 'utf-8')); + writeFileSync(pbxprojPath, preUISceneSource); + + const result = addSwiftFileToAppTarget(pbxprojPath, 'App', 'SceneDelegate.swift'); + + expect(result.added).toBe(true); + + // Reparse the written file and verify each section. + const project = loadXcodeProject(pbxprojPath); + project.parseSync(); + + const objects = project.hash.project.objects; + + const fileRefs = Object.entries(objects.PBXFileReference).filter(([k]) => !k.endsWith('_comment')); + expect(fileRefs.some(([, ref]) => typeof ref === 'object' && (ref as any).path === '"SceneDelegate.swift"')).toBe( + true, + ); + + const buildFiles = Object.entries(objects.PBXBuildFile).filter(([k]) => !k.endsWith('_comment')); + expect( + buildFiles.some(([k]) => (objects.PBXBuildFile as any)[`${k}_comment`]?.includes('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 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); + }); + + it('is a no-op when the file is already registered', () => { + writeFileSync(pbxprojPath, readFileSync(SHIPPED_PBXPROJ, 'utf-8')); + + const before = readFileSync(pbxprojPath, 'utf-8'); + const result = addSwiftFileToAppTarget(pbxprojPath, 'App', 'SceneDelegate.swift'); + const after = readFileSync(pbxprojPath, 'utf-8'); + + expect(result.added).toBe(false); + expect(after).toBe(before); + }); + + it('is idempotent across repeated runs', () => { + const preUISceneSource = stripSceneDelegate(readFileSync(SHIPPED_PBXPROJ, 'utf-8')); + writeFileSync(pbxprojPath, preUISceneSource); + + expect(addSwiftFileToAppTarget(pbxprojPath, 'App', 'SceneDelegate.swift').added).toBe(true); + expect(addSwiftFileToAppTarget(pbxprojPath, 'App', 'SceneDelegate.swift').added).toBe(false); + expect(addSwiftFileToAppTarget(pbxprojPath, 'App', 'SceneDelegate.swift').added).toBe(false); + }); + + it('throws when the target group cannot be found', () => { + writeFileSync(pbxprojPath, stripSceneDelegate(readFileSync(SHIPPED_PBXPROJ, 'utf-8'))); + + expect(() => addSwiftFileToAppTarget(pbxprojPath, 'DoesNotExist', 'SceneDelegate.swift')).toThrow( + /Could not find PBXGroup/, + ); + }); +}); From 3ab4139bd0b8863cadcb175180ea941f4c244f08 Mon Sep 17 00:00:00 2001 From: "Github Workflow (on behalf of theproducer)" Date: Fri, 31 Jul 2026 15:46:24 +0000 Subject: [PATCH 28/30] Release 8.5.0 --- CHANGELOG.md | 11 +++++++++++ android/CHANGELOG.md | 4 ++++ android/package.json | 4 ++-- cli/CHANGELOG.md | 10 ++++++++++ cli/package.json | 2 +- core/CHANGELOG.md | 4 ++++ core/package.json | 2 +- ios/CHANGELOG.md | 6 ++++++ ios/package.json | 4 ++-- lerna.json | 2 +- 10 files changed, 42 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41320fefa..a450e9376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31) + +### 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 + +- **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)) + ## [8.4.2](https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2) (2026-07-14) ### Bug Fixes diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 106d9c0c8..9b7407f1b 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31) + +**Note:** Version bump only for package @capacitor/android + ## [8.4.2](https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2) (2026-07-14) ### Bug Fixes diff --git a/android/package.json b/android/package.json index 285d9ec78..9311c332a 100644 --- a/android/package.json +++ b/android/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/android", - "version": "8.4.2", + "version": "8.5.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", @@ -23,7 +23,7 @@ "verify": "./gradlew clean lint build test -b capacitor/build.gradle" }, "peerDependencies": { - "@capacitor/core": "^8.4.0" + "@capacitor/core": "^8.5.0" }, "publishConfig": { "access": "public" diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 3fb75e17a..166dc16c6 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31) + +### 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 + +- **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)) + ## [8.4.2](https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2) (2026-07-14) **Note:** Version bump only for package @capacitor/cli diff --git a/cli/package.json b/cli/package.json index 627f846a3..77d2cde8c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/cli", - "version": "8.4.2", + "version": "8.5.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index a90269b59..772a593c8 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31) + +**Note:** Version bump only for package @capacitor/core + ## [8.4.2](https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2) (2026-07-14) **Note:** Version bump only for package @capacitor/core diff --git a/core/package.json b/core/package.json index 870ee47a7..b5db0ad35 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/core", - "version": "8.4.2", + "version": "8.5.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 9a30e6bb4..08e2603b1 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31) + +### Features + +- **ios:** UIScene Support ([#8536](https://github.com/ionic-team/capacitor/issues/8536)) ([3fa04a3](https://github.com/ionic-team/capacitor/commit/3fa04a357c92af34cd6fccb8124791963804a9dc)) + ## [8.4.2](https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2) (2026-07-14) **Note:** Version bump only for package @capacitor/ios diff --git a/ios/package.json b/ios/package.json index 5e0fbed66..41382350b 100644 --- a/ios/package.json +++ b/ios/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/ios", - "version": "8.4.2", + "version": "8.5.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", @@ -25,7 +25,7 @@ "xc:build:CapacitorCordova": "cd CapacitorCordova && xcodebuild && cd .." }, "peerDependencies": { - "@capacitor/core": "^8.4.0" + "@capacitor/core": "^8.5.0" }, "publishConfig": { "access": "public" diff --git a/lerna.json b/lerna.json index fbd4baa8e..900fe9a29 100644 --- a/lerna.json +++ b/lerna.json @@ -11,6 +11,6 @@ "tagVersionPrefix": "" } }, - "version": "8.4.2", + "version": "8.5.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } From 5e5bb3befc312477900252ab07e23b596f8cb0d1 Mon Sep 17 00:00:00 2001 From: Omri Willheim <48905545+omriwil@users.noreply.github.com> Date: Thu, 6 Aug 2026 18:11:13 +0300 Subject: [PATCH 29/30] fix(cli): use POSIX paths in CapApp-SPM Package.swift (#8549) Co-authored-by: Cursor --- cli/src/util/spm.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts index 65cd0f11f..93b1dd38f 100644 --- a/cli/src/util/spm.ts +++ b/cli/src/util/spm.ts @@ -12,6 +12,7 @@ import { getMajoriOSVersion } from '../ios/common'; import { logger } from '../log'; import type { Plugin } from '../plugin'; import { getPlatformElement, getPluginPlatform, getPluginType, PluginType } from '../plugin'; +import { convertToUnixPath } from '../util/fs'; import { runCommand } from '../util/subprocess'; export interface SwiftPlugin { @@ -122,7 +123,7 @@ let package = Package( if (getPluginType(plugin, config.ios.name) === PluginType.Cordova) { const platformTag = getPluginPlatform(plugin, config.ios.name); if (platformTag.$?.package) { - const relPath = relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath); + const relPath = convertToUnixPath(relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath)); packageSwiftText += `,\n .package(name: "${plugin.id}", path: "${relPath}")`; } else { const sourceFiles = getPlatformElement(plugin, config.ios.name, 'source-file'); @@ -136,7 +137,9 @@ let package = Package( const options = packageOptions[plugin.id]; const symlink = options?.symlink; const symlinkFolder = join('symlinks', plugin.name); - const relPath = 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)); } From 5ac4dd613ae989d8dc8738ea25b77efbd4fa21fe Mon Sep 17 00:00:00 2001 From: Manik Tyagi Date: Tue, 11 Aug 2026 01:18:36 +0530 Subject: [PATCH 30/30] fix(core): prevent removeListener from removing wrong listener (#8271) Co-authored-by: Eric Horodyski --- core/src/tests/web-plugin.spec.ts | 26 ++++++++++++++++++++++++++ core/src/web-plugin.ts | 4 +++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/core/src/tests/web-plugin.spec.ts b/core/src/tests/web-plugin.spec.ts index cb4a65ba0..1e6d4a7f8 100644 --- a/core/src/tests/web-plugin.spec.ts +++ b/core/src/tests/web-plugin.spec.ts @@ -181,4 +181,30 @@ describe('Web Plugin', () => { expect(handlerFunction).not.toHaveBeenCalled(); }); + + it('Should not remove a listener if it is not found', async () => { + const lf1 = (event: any) => { + console.log(event); + }; + const lf2 = (event: any) => { + console.log(event); + }; + const lf3 = (event: any) => { + console.log(event); + }; + + await plugin.addListener('test', lf1); + await plugin.addListener('test', lf2); + + const listenersBefore = plugin.getListeners()['test']; + expect(listenersBefore.length).toEqual(2); + + // Try to remove a listener that was never added + await (plugin as any).removeListener('test', lf3); + + const listenersAfter = plugin.getListeners()['test']; + expect(listenersAfter.length).toEqual(2); + expect(listenersAfter[0]).toBe(lf1); + expect(listenersAfter[1]).toBe(lf2); + }); }); diff --git a/core/src/web-plugin.ts b/core/src/web-plugin.ts index fbc8fcdd5..5fb324ee5 100644 --- a/core/src/web-plugin.ts +++ b/core/src/web-plugin.ts @@ -98,7 +98,9 @@ export class WebPlugin implements Plugin { } const index = listeners.indexOf(listenerFunc); - this.listeners[eventName].splice(index, 1); + if (index !== -1) { + this.listeners[eventName].splice(index, 1); + } // If there are no more listeners for this type of event, // remove the window listener