Skip to content

chore: sync upstream PR #8521 - chore(cli): migrate changes for Capacitor 9#81

Open
riderx wants to merge 83 commits into
plusfrom
sync/upstream-pr-8521
Open

chore: sync upstream PR #8521 - chore(cli): migrate changes for Capacitor 9#81
riderx wants to merge 83 commits into
plusfrom
sync/upstream-pr-8521

Conversation

@riderx

@riderx riderx commented Jul 2, 2026

Copy link
Copy Markdown
Member

Merge Conflict Resolution Required

The sync of upstream PR ionic-team#8521 from @jcesarmobile encountered merge conflicts.

Original PR: ionic-team#8521

What happened

  • Claude Code attempted to resolve the merge conflicts
  • This PR was created so CI and manual review can finish the sync safely

Synced from upstream by Capacitor+ Bot

Summary by CodeRabbit

  • New Features

    • Added support for a new app launch URL option in the CLI.
    • Improved plugin configuration handling with support for numeric values.
    • Expanded telemetry to report the iOS package manager used.
  • Bug Fixes

    • Improved Android and iOS runtime behavior for system bars, permissions, and web view messaging.
    • Updated CI timing and build reliability.
  • Breaking Changes

    • Raised minimum supported Android and iOS versions.
    • Updated Android and iOS packaging to the latest release line.

jworreth and others added 30 commits February 17, 2026 15:52
…m#8346)

Co-authored-by: jwo <jonathan.worreth@protonmail.ch>
Co-authored-by: Mark Anderson <mark.anderson@outsystems.com>
Co-authored-by: Steven Sherry <steven.r.sherry@gmail.com>
Co-authored-by: jcesarmobile <jcesarmobile@gmail.com>
Co-authored-by: Joseph Pender <joey.pender@outsystems.com>
jcesarmobile and others added 26 commits June 3, 2026 14:45
Co-authored-by: jcesarmobile <jcesarmobile@gmail.com>
@riderx

riderx commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Claude Code could not resolve this upstream sync completely. Please review the branch carefully before merging.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Beta npm build

Maintainers can publish one Capacitor Plus workspace package from this PR to npm for fast testing.

Comment /publish-beta <package> after the PR checks are green.

Examples:

/publish-beta core
/publish-beta cli
/publish-beta @capacitor-plus/core

If exactly one workspace package changed, /publish-beta without a package will use that package.

Packages:

  • core (@capacitor-plus/core)
  • cli (@capacitor-plus/cli)
  • android (@capacitor-plus/android)
  • ios (@capacitor-plus/ios)

The workflow will:

  • publish a prerelease package on the beta tag
  • add a pinned pr-81 dist-tag for the selected package
  • update this comment with the install command

Security note: beta publish is only enabled for branches inside this repository.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR rebrands the project from capacitor-plus to @capacitor/*, extracting Cordova compatibility into separate capacitor-cordova modules for Android and iOS wired via interceptor APIs, refactoring core Bridge classes accordingly, raising Android/iOS minimum SDK and deployment targets and Gradle tooling versions, updating CLI generation/run/telemetry/migration logic for Cordova gating, adding SystemBars inset-handling modes and getDouble config accessors, and applying minor formatting cleanups.

Changes

Package Rebrand and Release Metadata

Layer / File(s) Summary
Package identity, versions, and changelogs
android/package.json, cli/package.json, core/package.json, ios/package.json, */CHANGELOG.md, lerna.json
Packages renamed from @capacitor-plus/* to @capacitor/*, versioned 9.0.0-alpha.5, peer dependencies updated, changelogs refreshed, lerna allowBranch set to "next".

Android Cordova Extraction and Bridge Refactor

Layer / File(s) Summary
New capacitor-cordova module
android/capacitor-cordova/build.gradle, .../proguard-rules.pro, .../AndroidManifest.xml, android/settings.gradle
Adds a new Android library module for Cordova support and wires it into settings.gradle.
CordovaPlugin.java and mocks
android/capacitor-cordova/src/main/java/.../CordovaPlugin.java, MockCordovaInterfaceImpl.java, MockCordovaWebViewImpl.java
Implements a Capacitor plugin bootstrapping Cordova via a registered interceptor and forwarded lifecycle events; mock classes made package-private.
Bridge.java / MessageHandler.java interceptor refactor
android/capacitor/src/main/java/.../Bridge.java, MessageHandler.java
Removes direct Cordova wiring in favor of __CordovaPlugin lookups, registerInterceptor/getCallInterceptor, and interceptor-based postMessage dispatch; simplifies WebView version detection.
Core module Cordova removal
android/capacitor/build.gradle, proguard-rules.pro
Removes cordovaAndroidVersion default, Cordova framework dependency, and Cordova ProGuard rule; bumps AGP/SDK defaults.
Template/plugins Cordova cleanup
android-template/app/build.gradle, android-template/settings.gradle, capacitor-cordova-android-plugins/build.gradle
Removes targetSdkVersion, flatDir repository, and Cordova plugin dependency; updates SDK defaults.

iOS Cordova Extraction and Bridge Refactor

Layer / File(s) Summary
CordovaPlugin (Plugin.swift)
ios/CapacitorCordova/.../Plugin.swift, ios/CapacitorCordova.podspec, ios/Capacitor.podspec
Implements Cordova JS injection, config.xml parsing, plugin manager, and a "cordova" call interceptor.
CapacitorBridge interceptor wiring
CapacitorBridge.swift, CAPBridgeProtocol.swift, JS.swift, JSExport.swift, WebViewDelegationHandler.swift
Replaces Cordova plugin manager storage with cordovaIsPresent/callInterceptors and a public registerCallInterceptor API; removes Cordova JS export helpers.
View controller and descriptor Cordova removal
CAPBridgeViewController.swift, CAPInstanceDescriptor.h/.m/.swift, CAPApplicationDelegateProxy.swift, ConfigurationTests.swift
Bridge construction drops cordovaConfiguration; deploy-disabled state now queried via __CordovaPlugin; descriptor Cordova XML parsing removed.
Xcode project cleanup
Capacitor.xcodeproj/project.pbxproj, CAPBridgeViewController+CDVScreenOrientationDelegate.h, PluginCallAccessorTests.m
Removes obsolete Cordova delegate category and TmpViewController from the project.
iOS deployment target bump
*.xcodeproj/project.pbxproj, Podfile, Package.swift, pods_helpers.rb, AppDelegate.swift (templates)
Raises minimum iOS target to 16.0 and switches entrypoints to @main.

CLI Cordova Gating, Run/Live-Reload, and Telemetry

Layer / File(s) Summary
Android update Cordova gating
cli/src/android/update.ts
Gates Gradle/manifest/plugin-JSON Cordova generation on enableCordova; removes Kotlin detection.
iOS Cordova generation
cli/src/ios/update.ts, cli/src/util/cordova-ios.ts, iosplugin.ts, spm.ts, declarations.ts, config.ts
Delegates podspec/package/native-file generation to new cordova-ios utilities, gated by enableCordova; adds SPM packageOptions (symlink/moduleAliases); bumps min SDK versions.
Run/live-reload URL option
cli/src/tasks/run.ts, cli/src/util/livereload.ts, cli/src/index.ts
Replaces liveReload/host/port/https with a single validated url option.
Telemetry and IPC endpoint
cli/src/telemetry.ts, cli/test/telemetry.spec.ts, cli/src/ipc.ts
Adds ios_package_manager telemetry field; updates metrics endpoint/success status code.
Migration updates
cli/src/tasks/migrate.ts
Updates target versions, guards, breaking-change list/link, and adds AppDelegate/settings.gradle rewrites.

Build Tooling and SDK Version Upgrades

Layer / File(s) Summary
CI, Gradle wrapper, gradlew scripts
.github/workflows/test.yml, */gradle-wrapper.properties, */gradlew, */gradlew.bat, variables.gradle, scripts/publish-android.sh
Increases CI timeouts, bumps Gradle wrapper to 9.5.1, updates gradlew classpath/error handling, and scopes publish script to :capacitor-android tasks.

SystemBars Insets Handling and PluginConfig.getDouble

Layer / File(s) Summary
SystemBars insetsHandling mode
android/capacitor/src/main/java/.../SystemBars.java, SystemBarsTest.java
Replaces boolean inset flag with css/disable string mode; refactors setHidden bar selection.
PluginConfig.getDouble
PluginConfig.java, JSONUtils.java, PluginConfig.swift
Adds getDouble accessors on Android and iOS.

Minor Formatting and Logic Cleanups

Layer / File(s) Summary
Android formatting cleanups
BridgeWebChromeClient.java, FileUtils.java, WebViewLocalServer.java, CapacitorCookieManager.java, HttpRequestHandler.java, CapacitorHttpUrlConnection.java
Simplifies boolean expressions and removes SDK-conditional Base64 decoding.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebView
  participant WebViewDelegationHandler
  participant CapacitorBridge
  participant CordovaPlugin

  CordovaPlugin->>CapacitorBridge: registerCallInterceptor("cordova", handler)
  WebView->>WebViewDelegationHandler: postMessage({type:"cordova", service, action, ...})
  WebViewDelegationHandler->>CapacitorBridge: lookup callInterceptors[type]
  CapacitorBridge->>CordovaPlugin: invoke registered handler(payload)
  CordovaPlugin->>CordovaPlugin: resolve plugin command via selector
  CordovaPlugin-->>WebView: execute Cordova plugin command
Loading
sequenceDiagram
  participant CLI
  participant AndroidUpdate as android/update.ts
  participant Gradle as capacitor.settings.gradle
  participant Manifest as AndroidManifest

  CLI->>AndroidUpdate: updateAndroid(config, plugins)
  AndroidUpdate->>AndroidUpdate: enableCordova = cordovaPlugins.length > 0
  alt enableCordova true
    AndroidUpdate->>Gradle: include capacitor-cordova-android(-plugins)
    AndroidUpdate->>Manifest: generate Cordova manifest entries
  else enableCordova false
    AndroidUpdate->>Gradle: skip Cordova subprojects
  end
  AndroidUpdate-->>CLI: writePluginsJson(includeCordova)
Loading

Possibly related PRs

  • Cap-go/capacitor-plus#66: Modifies the same WebViewLocalServer.PathHandler header-building logic and SystemBars/CapacitorHttpUrlConnection code touched in this PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the upstream sync and Capacitor 9 migration as the main change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
cli/src/tasks/run.ts (1)

101-123: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restore the Android manifest when live-reload startup fails.

Line 104 enables the Android live-reload manifest state before run(...), but Lines 121-123 only revert capacitor.config.json on errors. If run(...) fails, the manifest can remain with the temporary cleartext/live-reload changes.

Proposed fix
+    let cordovaPlugins: Awaited<ReturnType<typeof getCordovaPlugins>> | undefined;
+    let androidManifestUpdated = false;
     try {
       if (options.sync) {
         await sync(config, platformName, false, true);
       }
-      const cordovaPlugins = await getCordovaPlugins(config, platformName);
+      cordovaPlugins = await getCordovaPlugins(config, platformName);
       if (options.url) {
         await CapLiveReloadHelper.editCapConfigForLiveReload(config, platformName, options);
         if (platformName === config.android.name) {
-          await await writeCordovaAndroidManifest(cordovaPlugins, config, platformName, true);
+          await writeCordovaAndroidManifest(cordovaPlugins, config, platformName, true);
+          androidManifestUpdated = true;
         }
       }
       await run(config, platformName, options);
@@
     } catch (e: any) {
       if (options.url) {
         await CapLiveReloadHelper.revertCapConfigForLiveReload();
+        if (androidManifestUpdated && platformName === config.android.name && cordovaPlugins) {
+          await writeCordovaAndroidManifest(cordovaPlugins, config, platformName, false);
+        }
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/tasks/run.ts` around lines 101 - 123, The live-reload startup path in
run() only reverts the Capacitor config on failure, leaving the temporary
Android manifest changes applied if run(config, platformName, options) throws.
Update the catch block in cli/src/tasks/run.ts to also restore the Android
manifest via writeCordovaAndroidManifest(...) when platformName matches
config.android.name and options.url is set, mirroring the cleanup already done
in the SIGINT path.
CHANGELOG.md (1)

6-116: 🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Unresolved merge conflict: duplicate/conflicting changelog entries.

Same pattern as android/CHANGELOG.md: 9.0.0-alpha.4 heading duplicated (Line 14 vs Line 37), 9.0.0-alpha.3 duplicated with conflicting compare ranges (Line 20 vs Line 49), the entire 9.0.0-alpha.0 block duplicated verbatim (Lines 20-35 vs 61-70), and 8.4.0 duplicated with conflicting compare links/content (Line 47 vs Line 72). As per coding guidelines, "Do not manually edit CHANGELOG.md; it is managed automatically by CI/CD" — revert this manual reconciliation and let CI regenerate it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 6 - 116, The CHANGELOG contains unresolved
merge-conflict duplication with repeated and conflicting release sections.
Remove the manually reconciled duplicates in the changelog blocks for the
repeated version headings and restore the file to the automatically generated
state; use the existing version headings such as 9.0.0-alpha.4, 9.0.0-alpha.3,
9.0.0-alpha.0, and 8.4.0 to locate the conflicting entries, and do not hand-edit
the changelog beyond reverting the manual merge.

Source: Coding guidelines

core/CHANGELOG.md (1)

6-102: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Regenerate the changelog instead of keeping duplicate release blocks.

This file repeats the 9.0.0-alpha.4, 8.4.0, and 8.3.4 sections, so the release history is no longer a single linear record. Please regenerate it from the release metadata source rather than hand-editing merged sections. As per coding guidelines, CHANGELOG.md: Do not manually edit CHANGELOG.md; it is managed automatically by CI/CD.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/CHANGELOG.md` around lines 6 - 102, The changelog contains duplicated
release sections, so restore a single linear history by regenerating
core/CHANGELOG.md from the automated release metadata source instead of
hand-editing it. Remove the repeated 9.0.0-alpha.4, 8.4.0, and 8.3.4 blocks and
ensure the generated output matches the standard changelog flow used for
`@capacitor/core`.

Sources: Coding guidelines, Linters/SAST tools

ios/CHANGELOG.md (1)

6-102: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Regenerate the changelog instead of keeping duplicate release blocks.

This file repeats the 9.0.0-alpha.4, 8.4.0, and 8.3.4 sections, so the release history is inconsistent. Please regenerate it from the release metadata source rather than hand-editing merged sections. As per coding guidelines, CHANGELOG.md: Do not manually edit CHANGELOG.md; it is managed automatically by CI/CD.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/CHANGELOG.md` around lines 6 - 102, The ios/CHANGELOG.md entry has
duplicated release sections, so the history is inconsistent. Regenerate the
changelog from the release metadata source instead of manually merging or
editing sections, and ensure the duplicate blocks for the repeated versions are
removed so the final output is a single, correctly ordered release history.

Sources: Coding guidelines, Linters/SAST tools

android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java (1)

177-192: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Undocumented SDK-gated insets acquisition — clarify rationale.

The choice to call ViewCompat.getRootWindowInsets(v) only on VANILLA_ICE_CREAM+ and fall back to WindowInsetsCompat.CONSUMED (i.e., zero insets) on older devices means the initial safe-area CSS values will always be 0 on older Android until the window insets listener later fires with real values. This is a legitimate workaround if getRootWindowInsets() is unreliable pre-attach on older APIs, but the behavior is non-obvious and undocumented.

Consider adding a short comment explaining why older SDKs intentionally skip the real insets query here, to avoid future maintainers "fixing" this into a regression.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java`
around lines 177 - 192, The SDK-gated insets lookup in SystemBars is
intentionally falling back to zero initial safe-area values on older Android,
but the rationale is not documented. Add a short inline comment near the
Build.VERSION.SDK_INT branch in SystemBars’s insets handling to explain why
ViewCompat.getRootWindowInsets(v) is only used on VANILLA_ICE_CREAM+ and why
older devices deliberately use WindowInsetsCompat.CONSUMED until the listener
updates the CSS.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/test.yml:
- Around line 129-131: The CI workflow is missing the native podspec validation
step, so restore the `Validate native podspec` job in the test workflow or add
an equivalent explicit lint step elsewhere. Use the existing `test-android`
workflow section to reintroduce the `sh ./scripts/native-podspec.sh lint` check
so both `Capacitor.podspec` and `CapacitorCordova.podspec` are validated again,
since `ios/package.json` verify only covers Xcode builds.

In `@android-template/gradle/wrapper/gradle-wrapper.properties`:
- Around line 3-6: The Gradle wrapper settings in gradle-wrapper.properties
still leave wrapper-download retries disabled, which can make CI more fragile
during distribution fetches. Update the wrapper configuration by changing the
existing retries setting to a non-zero value and keep the current backoff tuning
consistent with it. Use the Gradle wrapper properties keys already present in
this file, especially retries and retryBackOffMs, so the download behavior is
resilient without changing the distribution URL or timeout.

In
`@android/capacitor-cordova/src/main/java/com/getcapacitor/cordova/CordovaPlugin.java`:
- Around line 69-74: Persist the Cordova callback bundle in CordovaPlugin by
overriding saveInstanceState() alongside restoreState(Bundle), since
restoreState currently only restores whatever Plugin.saveInstanceState()
provides. Update the CordovaPlugin class to store the cordovaInterface state
into the Bundle during saving, then read it back in restoreState before calling
cordovaInterface.restoreInstanceState(state), so activity-result callbacks
survive process death.

In `@android/capacitor/src/main/java/com/getcapacitor/Bridge.java`:
- Around line 1512-1557: The core interceptors are being attached too late: new
Bridge(...) starts WebView loading before "message" and "js.error" are
registered, so early bridge messages can be ignored. Move the
bridge.registerInterceptor(...) setup for both handlers to happen before any
WebView loading starts inside Bridge initialization, keeping the existing
behavior in the Bridge constructor and MessageHandler flow intact.
- Around line 293-307: The WebView version check in
Bridge.isMinimumWebViewInstalled currently assumes
WebView.getCurrentWebViewPackage() and its versionName are always present, which
can crash during startup. Add a null guard at the start of
isMinimumWebViewInstalled to return false when the PackageInfo or versionName is
missing, then keep the existing parsing and min-version comparison logic
unchanged for the valid case.

In `@android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java`:
- Around line 22-47: The current tests cover only the show path in
SystemBarsTest, but the refactored hide branch in SystemBars is not exercised.
Update invokeSetHidden to accept a hide boolean so tests can call the same logic
for both paths, then add hide-path tests that verify the correct
WindowInsetsControllerCompat.hide calls for empty, StatusBar, and NavigationBar
inputs while keeping the existing show-path assertions intact.

In `@android/CHANGELOG.md`:
- Around line 6-84: The changelog edit has unresolved merge-conflict duplication
with repeated version headings and conflicting compare links in
android/CHANGELOG.md. Revert the manual changes to this file rather than trying
to reconcile entries by hand, since it is CI/CD-managed. Use the duplicated
release blocks like the repeated 9.0.0-alpha.4, 9.0.0-alpha.3, 9.0.0-alpha.0,
8.4.0, and 8.3.4 sections as the indicators that this should be reset to the
auto-generated version.

In `@cli/CHANGELOG.md`:
- Around line 6-103: The changelog contains duplicated release sections for the
same versions, which makes the history inconsistent and can trigger lint
failures. Regenerate the entire cli/CHANGELOG.md from the release metadata
source instead of editing it manually, and ensure the duplicate blocks for
versions like 9.0.0-alpha.4, 8.4.0, and 8.3.4 are removed. Keep the file aligned
with the automated changelog process used by CI/CD.

In `@cli/src/ios/update.ts`:
- Around line 95-99: The incompatible Cordova plugin warning is currently gated
by enableCordova inside updateIos behavior, so it never runs when only
incompatible Cordova plugins are installed. Move the incompatible plugin
detection and printPlugins call out of the enableCordova block in the update
flow, while keeping logCordovaManualSteps and other Cordova-only steps inside
it. Use the existing getPluginType, PluginType.Incompatible, and printPlugins
symbols in cli/src/ios/update.ts to place the warning in the right spot.

In `@cli/src/tasks/migrate.ts`:
- Around line 125-127: The migration confirmation text in migrate and the
logPrompt message is mixing Android platform version wording with SDK/minVersion
details. Update the prompt shown around migrateconfirm to use consistent Android
terminology that matches the config default of Android minVersion 26, and avoid
mentioning “Android 17 (SDK 37)” in the confirmation copy. Keep the iOS version
reference, but make the Android wording clearly refer to the migration target
defined by the migration flow rather than target SDK or platform version.
- Around line 92-95: The version gate in migrate.ts is too loose because
`capMajor < 8` still allows Capacitor 9+ and parse failures (`NaN`) to continue
into a destructive migration. Update the `checkCapacitorMajorVersion` check in
`migrate` to require exactly Capacitor 8 before any project file mutations, and
keep the `fatal` path for anything other than 8, including invalid or missing
version parses.

In `@cli/src/telemetry.ts`:
- Around line 85-90: The debug log in telemetry currently prints the full `data`
payload, which can expose sensitive `cmd.opts()` values through `data.options`.
Update `cli/src/telemetry.ts` so the `debug('metric payload: %O', ...)` call
logs a redacted/sanitized copy of the payload instead of the raw `data` object.
Use the `data` assembly area and the `debug` statement to locate the change, and
ensure secrets like keystore and signing credentials are masked before logging.

In `@cli/src/util/cordova-ios.ts`:
- Around line 22-27: The Cordova podspec/package generation flow is starting
asynchronous file writes and returning early, so consumers can continue before
the files exist. Update generateCordovaPodspecs and the underlying
generateCordovaPodspec helper(s) to await all async writes (for example by
collecting the per-plugin write promises and awaiting them before returning) so
pod install and SPM generation only proceed after the files are fully written.
- Around line 271-278: Escape the preference name before constructing the RegExp
in replaceFrameworkVariables, since only the leading $ is currently handled and
names with other regex metacharacters can break matching or replace the wrong
text. Update the replacement logic in replaceFrameworkVariables so it safely
escapes the full preference.$.name before passing it to new RegExp, while
keeping the existing default substitution behavior unchanged.

In `@ios/Capacitor/Capacitor/CAPInstanceDescriptor.swift`:
- Around line 144-147: The public `cordovaDeployDisabled` value in
`InstanceDescriptor` is hardcoded and no longer reflects the configuration
contract exposed through `CAPInstanceConfiguration`. Update the
`InstanceDescriptor` property and its initialization flow so the flag is still
parsed and propagated from the descriptor, or remove/deprecate the corresponding
public configuration surface consistently. Use the `CAPInstanceDescriptor` and
`CAPInstanceConfiguration` symbols to trace the property through the
descriptor-to-configuration path and keep the contract aligned.

In `@ios/Capacitor/Capacitor/JS.swift`:
- Around line 17-22: The JSCall type is unnecessarily public even though it is
only used by bridge internals. Change JSCall in JS.swift to internal unless you
explicitly intend to expose it as part of the public API, and keep its members
aligned with that visibility. If external access is not required, reduce the
type’s access level so the bridge implementation remains encapsulated.

In `@ios/CapacitorCordova/CapacitorCordova/Classes/Public/Plugin.swift`:
- Line 43: The Cordova args cast in the plugin argument parsing is too generic;
update the `actionArgs` extraction in `Plugin.swift` to use a concrete `[Any]`
type instead of `Array`, keeping the rest of the `dict["actionArgs"]` handling
unchanged. This applies to the `args` assignment inside the `Plugin` code path
and should preserve the existing default empty array behavior.

In `@lerna.json`:
- Line 7: The release branch guard in lerna.json is too restrictive because
allowBranch only permits next, which blocks the automated versioning flow on
plus. Update the Lerna configuration so the allowBranch setting includes plus
alongside next, keeping npx lerna version compatible with the bump_version
workflow.

---

Outside diff comments:
In `@android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java`:
- Around line 177-192: The SDK-gated insets lookup in SystemBars is
intentionally falling back to zero initial safe-area values on older Android,
but the rationale is not documented. Add a short inline comment near the
Build.VERSION.SDK_INT branch in SystemBars’s insets handling to explain why
ViewCompat.getRootWindowInsets(v) is only used on VANILLA_ICE_CREAM+ and why
older devices deliberately use WindowInsetsCompat.CONSUMED until the listener
updates the CSS.

In `@CHANGELOG.md`:
- Around line 6-116: The CHANGELOG contains unresolved merge-conflict
duplication with repeated and conflicting release sections. Remove the manually
reconciled duplicates in the changelog blocks for the repeated version headings
and restore the file to the automatically generated state; use the existing
version headings such as 9.0.0-alpha.4, 9.0.0-alpha.3, 9.0.0-alpha.0, and 8.4.0
to locate the conflicting entries, and do not hand-edit the changelog beyond
reverting the manual merge.

In `@cli/src/tasks/run.ts`:
- Around line 101-123: The live-reload startup path in run() only reverts the
Capacitor config on failure, leaving the temporary Android manifest changes
applied if run(config, platformName, options) throws. Update the catch block in
cli/src/tasks/run.ts to also restore the Android manifest via
writeCordovaAndroidManifest(...) when platformName matches config.android.name
and options.url is set, mirroring the cleanup already done in the SIGINT path.

In `@core/CHANGELOG.md`:
- Around line 6-102: The changelog contains duplicated release sections, so
restore a single linear history by regenerating core/CHANGELOG.md from the
automated release metadata source instead of hand-editing it. Remove the
repeated 9.0.0-alpha.4, 8.4.0, and 8.3.4 blocks and ensure the generated output
matches the standard changelog flow used for `@capacitor/core`.

In `@ios/CHANGELOG.md`:
- Around line 6-102: The ios/CHANGELOG.md entry has duplicated release sections,
so the history is inconsistent. Regenerate the changelog from the release
metadata source instead of manually merging or editing sections, and ensure the
duplicate blocks for the repeated versions are removed so the final output is a
single, correctly ordered release history.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 636475cf-e9e4-42b1-b588-2d979ddfb583

📥 Commits

Reviewing files that changed from the base of the PR and between 51a1030 and 47a7921.

⛔ Files ignored due to path filters (2)
  • android-template/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
  • android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
📒 Files selected for processing (87)
  • .github/workflows/test.yml
  • CHANGELOG.md
  • android-template/app/build.gradle
  • android-template/build.gradle
  • android-template/gradle/wrapper/gradle-wrapper.properties
  • android-template/gradlew
  • android-template/gradlew.bat
  • android-template/settings.gradle
  • android-template/variables.gradle
  • android/CHANGELOG.md
  • android/capacitor-cordova/build.gradle
  • android/capacitor-cordova/proguard-rules.pro
  • android/capacitor-cordova/src/main/AndroidManifest.xml
  • android/capacitor-cordova/src/main/java/com/getcapacitor/cordova/CapacitorCordovaCookieManager.java
  • android/capacitor-cordova/src/main/java/com/getcapacitor/cordova/CordovaPlugin.java
  • android/capacitor-cordova/src/main/java/com/getcapacitor/cordova/MockCordovaInterfaceImpl.java
  • android/capacitor-cordova/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java
  • android/capacitor/build.gradle
  • android/capacitor/proguard-rules.pro
  • android/capacitor/src/main/java/com/getcapacitor/Bridge.java
  • android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
  • android/capacitor/src/main/java/com/getcapacitor/FileUtils.java
  • android/capacitor/src/main/java/com/getcapacitor/MessageHandler.java
  • android/capacitor/src/main/java/com/getcapacitor/PluginConfig.java
  • android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java
  • android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java
  • android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
  • android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java
  • android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java
  • android/capacitor/src/main/java/com/getcapacitor/util/JSONUtils.java
  • android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java
  • android/gradle/wrapper/gradle-wrapper.properties
  • android/gradlew
  • android/gradlew.bat
  • android/package.json
  • android/settings.gradle
  • capacitor-cordova-android-plugins/build.gradle
  • cli/CHANGELOG.md
  • cli/package.json
  • cli/src/android/update.ts
  • cli/src/config.ts
  • cli/src/declarations.ts
  • cli/src/index.ts
  • cli/src/ios/update.ts
  • cli/src/ipc.ts
  • cli/src/tasks/migrate.ts
  • cli/src/tasks/run.ts
  • cli/src/telemetry.ts
  • cli/src/util/cordova-ios.ts
  • cli/src/util/iosplugin.ts
  • cli/src/util/livereload.ts
  • cli/src/util/spm.ts
  • cli/test/telemetry.spec.ts
  • core/CHANGELOG.md
  • core/package.json
  • ios-pods-template/App/App.xcodeproj/project.pbxproj
  • ios-pods-template/App/App/AppDelegate.swift
  • ios-pods-template/App/Podfile
  • ios-spm-template/App/App.xcodeproj/project.pbxproj
  • ios-spm-template/App/App/AppDelegate.swift
  • ios-spm-template/App/CapApp-SPM/Package.swift
  • ios/CHANGELOG.md
  • ios/Capacitor.podspec
  • ios/Capacitor/Capacitor.xcodeproj/project.pbxproj
  • ios/Capacitor/Capacitor/CAPApplicationDelegateProxy.swift
  • ios/Capacitor/Capacitor/CAPBridgeProtocol.swift
  • ios/Capacitor/Capacitor/CAPBridgeViewController+CDVScreenOrientationDelegate.h
  • ios/Capacitor/Capacitor/CAPBridgeViewController+CDVScreenOrientationDelegate.m
  • ios/Capacitor/Capacitor/CAPBridgeViewController.swift
  • ios/Capacitor/Capacitor/CAPInstanceDescriptor.h
  • ios/Capacitor/Capacitor/CAPInstanceDescriptor.m
  • ios/Capacitor/Capacitor/CAPInstanceDescriptor.swift
  • ios/Capacitor/Capacitor/CapacitorBridge.swift
  • ios/Capacitor/Capacitor/JS.swift
  • ios/Capacitor/Capacitor/JSExport.swift
  • ios/Capacitor/Capacitor/PluginConfig.swift
  • ios/Capacitor/Capacitor/TmpViewController.swift
  • ios/Capacitor/Capacitor/WebViewDelegationHandler.swift
  • ios/Capacitor/CapacitorTests/ConfigurationTests.swift
  • ios/Capacitor/CapacitorTests/PluginCallAccessorTests.m
  • ios/CapacitorCordova.podspec
  • ios/CapacitorCordova/CapacitorCordova.xcodeproj/project.pbxproj
  • ios/CapacitorCordova/CapacitorCordova/Classes/Public/Plugin.swift
  • ios/package.json
  • ios/scripts/pods_helpers.rb
  • lerna.json
  • scripts/publish-android.sh
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)
💤 Files with no reviewable changes (10)
  • ios/Capacitor/CapacitorTests/ConfigurationTests.swift
  • ios/Capacitor/Capacitor/TmpViewController.swift
  • ios/Capacitor/Capacitor/JSExport.swift
  • ios/Capacitor/Capacitor/CAPApplicationDelegateProxy.swift
  • ios/Capacitor/Capacitor/CAPBridgeViewController+CDVScreenOrientationDelegate.m
  • ios/Capacitor/Capacitor/CAPBridgeViewController+CDVScreenOrientationDelegate.h
  • android-template/settings.gradle
  • ios/Capacitor/Capacitor/CAPInstanceDescriptor.m
  • android/capacitor/proguard-rules.pro
  • android-template/app/build.gradle

Comment on lines +129 to 131
# - name: Validate native podspec
# run: sh ./scripts/native-podspec.sh lint
test-android:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== workflow snippet ==\n'
sed -n '110,145p' .github/workflows/test.yml

printf '\n== podspec lint script ==\n'
fd -a 'native-podspec.sh|podspec' scripts ios .github -t f 2>/dev/null || true

printf '\n== references to native-podspec ==\n'
rg -n "native-podspec|podspec lint|Validate native podspec|CapacitorCordova.podspec|Capacitor.podspec" .github scripts ios -S

Repository: Cap-go/capacitor-plus

Length of output: 2134


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== scripts/native-podspec.sh ==\n'
cat -n scripts/native-podspec.sh

printf '\n== ios/package.json ==\n'
cat -n ios/package.json

printf '\n== any podspec-related scripts in package.json files ==\n'
rg -n '"verify"|podspec|lint' package.json ios/package.json . -g 'package.json' -S

Repository: Cap-go/capacitor-plus

Length of output: 4208


Restore podspec lint in CI — commenting out Validate native podspec removes checks for both ios/Capacitor.podspec and ios/CapacitorCordova.podspec; ios/package.json’s verify script only runs Xcode builds, so add this step back or replace it with an explicit lint elsewhere.

🧰 Tools
🪛 zizmor (1.26.1)

[info] 131-131: workflow or action definition without a name (anonymous-definition): this job

(anonymous-definition)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 129 - 131, The CI workflow is
missing the native podspec validation step, so restore the `Validate native
podspec` job in the test workflow or add an equivalent explicit lint step
elsewhere. Use the existing `test-android` workflow section to reintroduce the
`sh ./scripts/native-podspec.sh lint` check so both `Capacitor.podspec` and
`CapacitorCordova.podspec` are validated again, since `ios/package.json` verify
only covers Xcode builds.

Comment on lines +3 to +6
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-all.zip
networkTimeout=10000
retries=0
retryBackOffMs=500

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Retries left at the Gradle-generated default (disabled) — consider enabling for CI resilience.

retries=0 is Gradle's stock default (retrying is disabled by default), so this isn't a misconfiguration. Given this same PR is bumping multiple CI job timeouts (likely due to observed flakiness), enabling wrapper-download retries could reduce transient network failures during distribution download in CI.

♻️ Enable wrapper download retries
 networkTimeout=10000
-retries=0
+retries=3
 retryBackOffMs=500
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-all.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-all.zip
networkTimeout=10000
retries=3
retryBackOffMs=500
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android-template/gradle/wrapper/gradle-wrapper.properties` around lines 3 -
6, The Gradle wrapper settings in gradle-wrapper.properties still leave
wrapper-download retries disabled, which can make CI more fragile during
distribution fetches. Update the wrapper configuration by changing the existing
retries setting to a non-zero value and keep the current backoff tuning
consistent with it. Use the Gradle wrapper properties keys already present in
this file, especially retries and retryBackOffMs, so the download behavior is
resilient without changing the distribution URL or timeout.

Comment on lines +69 to +74
@Override
protected void restoreState(Bundle state) {
if (state != null) {
cordovaInterface.restoreInstanceState(state);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check base Plugin contract for saveInstanceState/restoreState pairing.
rg -n "saveInstanceState|restoreState" android/capacitor/src/main/java/com/getcapacitor/Plugin.java android/capacitor/src/main/java/com/getcapacitor/Bridge.java

Repository: Cap-go/capacitor-plus

Length of output: 1047


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the CordovaPlugin class and the base Plugin lifecycle methods.
sed -n '1,180p' android/capacitor-cordova/src/main/java/com/getcapacitor/cordova/CordovaPlugin.java
printf '\n--- Plugin lifecycle ---\n'
sed -n '900,970p' android/capacitor/src/main/java/com/getcapacitor/Plugin.java
printf '\n--- Bridge restore/save paths ---\n'
sed -n '1010,1055p' android/capacitor/src/main/java/com/getcapacitor/Bridge.java
sed -n '1510,1535p' android/capacitor/src/main/java/com/getcapacitor/Bridge.java

Repository: Cap-go/capacitor-plus

Length of output: 10818


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find any saveInstanceState override in the Cordova plugin package.
rg -n "saveInstanceState\(" android/capacitor-cordova/src/main/java/com/getcapacitor/cordova

Repository: Cap-go/capacitor-plus

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Cordova plugin class and lifecycle hooks directly.
sed -n '1,140p' android/capacitor-cordova/src/main/java/com/getcapacitor/cordova/CordovaPlugin.java
printf '\n---\n'
sed -n '900,960p' android/capacitor/src/main/java/com/getcapacitor/Plugin.java
printf '\n---\n'
sed -n '1018,1048p' android/capacitor/src/main/java/com/getcapacitor/Bridge.java
printf '\n---\n'
sed -n '1518,1530p' android/capacitor/src/main/java/com/getcapacitor/Bridge.java

Repository: Cap-go/capacitor-plus

Length of output: 9291


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether CordovaPlugin defines a saveInstanceState override anywhere in the file.
rg -n "saveInstanceState|restoreState" android/capacitor-cordova/src/main/java/com/getcapacitor/cordova/CordovaPlugin.java

Repository: Cap-go/capacitor-plus

Length of output: 210


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Cordova interface implementation for any saved-state handling.
rg -n "class MockCordovaInterfaceImpl|restoreInstanceState|saveInstanceState|getActivityResultCallback|onActivityResult" android/capacitor-cordova/src/main/java

Repository: Cap-go/capacitor-plus

Length of output: 1007


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the Cordova interface implementation if present.
fd -a "MockCordovaInterfaceImpl.java" android/capacitor-cordova/src/main/java

Repository: Cap-go/capacitor-plus

Length of output: 273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the Cordova plugin package and inspect any related implementation files.
find android/capacitor-cordova/src/main/java -type f | sort

Repository: Cap-go/capacitor-plus

Length of output: 531


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the mock Cordova interface implementation and any saved-state methods.
fd -a "MockCordovaInterfaceImpl.java|MockCordovaWebViewImpl.java" android/capacitor-cordova/src/main/java

Repository: Cap-go/capacitor-plus

Length of output: 385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the implementation of restoreInstanceState to see what bundle shape it expects.
rg -n "restoreInstanceState\\(" -g'*.java' -g'*.kt' android

Repository: Cap-go/capacitor-plus

Length of output: 561


Persist the Cordova callback bundle here
restoreState(Bundle) only gets the base Plugin.saveInstanceState() payload unless this plugin overrides saveInstanceState() to persist cordovaInterface state, so Cordova activity-result callbacks can be lost after process death.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/capacitor-cordova/src/main/java/com/getcapacitor/cordova/CordovaPlugin.java`
around lines 69 - 74, Persist the Cordova callback bundle in CordovaPlugin by
overriding saveInstanceState() alongside restoreState(Bundle), since
restoreState currently only restores whatever Plugin.saveInstanceState()
provides. Update the CordovaPlugin class to store the cordovaInterface state
into the Bundle during saving, then read it back in restoreState before calling
cordovaInterface.restoreInstanceState(state), so activity-result callbacks
survive process death.

Comment on lines 293 to 307
@SuppressLint("WebViewApiAvailability")
public boolean isMinimumWebViewInstalled() {
PackageManager pm = getContext().getPackageManager();

// Check getCurrentWebViewPackage() directly if above Android 8
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PackageInfo info = WebView.getCurrentWebViewPackage();
Pattern pattern = Pattern.compile("(\\d+)");
Matcher matcher = pattern.matcher(info.versionName);
if (matcher.find()) {
String majorVersionStr = matcher.group(0);
int majorVersion = Integer.parseInt(majorVersionStr);
if (info.packageName.equals("com.huawei.webview")) {
return majorVersion >= config.getMinHuaweiWebViewVersion();
}
return majorVersion >= config.getMinWebViewVersion();
} else {
return false;
}
}

// Otherwise manually check WebView versions
try {
PackageInfo info = InternalUtils.getPackageInfo(pm, "com.android.chrome");
String majorVersionStr = info.versionName.split("\\.")[0];
int majorVersion = Integer.parseInt(majorVersionStr);
return majorVersion >= config.getMinWebViewVersion();
} catch (Exception ex) {
Logger.warn("Unable to get package info for 'com.google.android.webview'" + ex.toString());
}

try {
PackageInfo info = InternalUtils.getPackageInfo(pm, "com.android.webview");
String majorVersionStr = info.versionName.split("\\.")[0];
PackageInfo info = WebView.getCurrentWebViewPackage();
Pattern pattern = Pattern.compile("(\\d+)");
Matcher matcher = pattern.matcher(info.versionName);
if (matcher.find()) {
String majorVersionStr = matcher.group(0);
int majorVersion = Integer.parseInt(majorVersionStr);
if (info.packageName.equals("com.huawei.webview")) {
return majorVersion >= config.getMinHuaweiWebViewVersion();
}
return majorVersion >= config.getMinWebViewVersion();
} catch (Exception ex) {
Logger.warn("Unable to get package info for 'com.android.webview'" + ex.toString());
}

final int amazonFireMajorWebViewVersion = extractWebViewMajorVersion(pm, "com.amazon.webview.chromium");
if (amazonFireMajorWebViewVersion >= config.getMinWebViewVersion()) {
return true;
}

// Could not detect any webview, return false
return false;
}

private int extractWebViewMajorVersion(final PackageManager pm, final String webViewPackageName) {
try {
final PackageInfo info = InternalUtils.getPackageInfo(pm, webViewPackageName);
final String majorVersionStr = info.versionName.split("\\.")[0];
final int majorVersion = Integer.parseInt(majorVersionStr);
return majorVersion;
} catch (Exception ex) {
Logger.warn(String.format("Unable to get package info for '%s' with err '%s'", webViewPackageName, ex));
} else {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Bridge.java around isMinimumWebViewInstalled()\n'
sed -n '260,330p' android/capacitor/src/main/java/com/getcapacitor/Bridge.java

printf '\n## Search for getCurrentWebViewPackage() and versionName parsing\n'
rg -n "getCurrentWebViewPackage|versionName|isMinimumWebViewInstalled" android/capacitor/src/main/java -S

printf '\n## Search for any existing null guards around WebView package info\n'
rg -n "PackageInfo info = WebView.getCurrentWebViewPackage|info == null|versionName == null" android/capacitor/src/main/java -S

Repository: Cap-go/capacitor-plus

Length of output: 4627


🌐 Web query:

Android WebView.getCurrentWebViewPackage null return PackageInfo.versionName null API contract

💡 Result:

The method WebView.getCurrentWebViewPackage returns a PackageInfo object describing the package currently providing WebView functionality [1]. According to the Android API contract, this method can return null [2][3]. Common reasons for a null return value include: - Device Configuration: The device may be set up in a way that does not support WebView, such as certain Wear OS devices or specific stripped-down system images where no WebView provider is installed or enabled [2]. - API Level Compatibility: The API was introduced in Android 8.0 (API level 26) [4][5]. While it is available in later versions, calling it on devices below this level (without compatibility handling) will not function as intended via the framework method [3]. Regarding PackageInfo.versionName returning null: Even when WebView.getCurrentWebViewPackage returns a non-null PackageInfo object, the versionName field within that object can be null [6]. This occurs because the versionName is a string field declared in the AndroidManifest.xml of the WebView provider package. If the provider package developer does not explicitly define a versionName in their manifest, or if the system fails to retrieve it, the field remains null. Best Practices: - Always null-check the result of WebView.getCurrentWebViewPackage [2]. - If you need to access WebView package information in a backward-compatible and safer way, use WebViewCompat.getCurrentWebViewPackage(Context) from the AndroidX Webkit library [7]. This library handles internal complexities across different Android API levels [3][8]. - When accessing fields like versionName, perform defensive coding to handle potential null values to avoid NullPointerExceptions [6].

Citations:


Guard WebView package info before parsing
WebView.getCurrentWebViewPackage() and info.versionName can both be null here; return false before parsing so startup falls back to the error page instead of crashing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/capacitor/src/main/java/com/getcapacitor/Bridge.java` around lines
293 - 307, The WebView version check in Bridge.isMinimumWebViewInstalled
currently assumes WebView.getCurrentWebViewPackage() and its versionName are
always present, which can crash during startup. Add a null guard at the start of
isMinimumWebViewInstalled to return false when the PackageInfo or versionName is
missing, then keep the existing parsing and min-version comparison logic
unchanged for the valid case.

Comment on lines +1512 to 1557
Bridge bridge = new Bridge(activity, serverPath, fragment, webView, plugins, pluginInstances, config);

if (webView instanceof CapacitorWebView capacitorWebView) {
capacitorWebView.setBridge(bridge);
}

bridge.setCordovaWebView(mockWebView);
bridge.setWebViewListeners(webViewListeners);
bridge.setRouteProcessor(routeProcessor);

if (instanceState != null) {
PluginHandle maybeCordova = bridge.getPlugin("__CordovaPlugin");
if (maybeCordova != null) {
maybeCordova.getInstance().restoreState(instanceState);
}
bridge.restoreInstanceState(instanceState);
}

bridge.registerInterceptor("message", (postData) -> {
try {
String callbackId = postData.getString("callbackId");
String pluginId = postData.getString("pluginId");
String methodName = postData.getString("methodName");
JSObject methodData = postData.getJSObject("options", new JSObject());

Logger.verbose(
Logger.tags("Plugin"),
"To native (Capacitor plugin): callbackId: " +
callbackId +
", pluginId: " +
pluginId +
", methodName: " +
methodName
);

PluginCall call = new PluginCall(bridge.msgHandler, pluginId, callbackId, methodName, methodData);
bridge.callPluginMethod(pluginId, methodName, call);
} catch (JSONException e) {
Logger.error(e.getMessage());
}
});

bridge.registerInterceptor("js.error", (postData) -> {
Logger.error("JavaScript Error: " + postData.toString());
});

return bridge;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Register core interceptors before starting WebView loading.

new Bridge(...) starts loading the WebView from the constructor, but "message" and "js.error" interceptors are registered only afterward. Early JS bridge calls can be dropped because MessageHandler.postMessage() ignores messages with no registered interceptor.

Proposed fix
-        this.loadWebView();
     }
             bridge.registerInterceptor("js.error", (postData) -> {
                 Logger.error("JavaScript Error: " + postData.toString());
             });
 
+            bridge.loadWebView();
             return bridge;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Bridge bridge = new Bridge(activity, serverPath, fragment, webView, plugins, pluginInstances, config);
if (webView instanceof CapacitorWebView capacitorWebView) {
capacitorWebView.setBridge(bridge);
}
bridge.setCordovaWebView(mockWebView);
bridge.setWebViewListeners(webViewListeners);
bridge.setRouteProcessor(routeProcessor);
if (instanceState != null) {
PluginHandle maybeCordova = bridge.getPlugin("__CordovaPlugin");
if (maybeCordova != null) {
maybeCordova.getInstance().restoreState(instanceState);
}
bridge.restoreInstanceState(instanceState);
}
bridge.registerInterceptor("message", (postData) -> {
try {
String callbackId = postData.getString("callbackId");
String pluginId = postData.getString("pluginId");
String methodName = postData.getString("methodName");
JSObject methodData = postData.getJSObject("options", new JSObject());
Logger.verbose(
Logger.tags("Plugin"),
"To native (Capacitor plugin): callbackId: " +
callbackId +
", pluginId: " +
pluginId +
", methodName: " +
methodName
);
PluginCall call = new PluginCall(bridge.msgHandler, pluginId, callbackId, methodName, methodData);
bridge.callPluginMethod(pluginId, methodName, call);
} catch (JSONException e) {
Logger.error(e.getMessage());
}
});
bridge.registerInterceptor("js.error", (postData) -> {
Logger.error("JavaScript Error: " + postData.toString());
});
return bridge;
Bridge bridge = new Bridge(activity, serverPath, fragment, webView, plugins, pluginInstances, config);
if (webView instanceof CapacitorWebView capacitorWebView) {
capacitorWebView.setBridge(bridge);
}
bridge.setWebViewListeners(webViewListeners);
bridge.setRouteProcessor(routeProcessor);
if (instanceState != null) {
PluginHandle maybeCordova = bridge.getPlugin("__CordovaPlugin");
if (maybeCordova != null) {
maybeCordova.getInstance().restoreState(instanceState);
}
bridge.restoreInstanceState(instanceState);
}
bridge.registerInterceptor("message", (postData) -> {
try {
String callbackId = postData.getString("callbackId");
String pluginId = postData.getString("pluginId");
String methodName = postData.getString("methodName");
JSObject methodData = postData.getJSObject("options", new JSObject());
Logger.verbose(
Logger.tags("Plugin"),
"To native (Capacitor plugin): callbackId: " +
callbackId +
", pluginId: " +
pluginId +
", methodName: " +
methodName
);
PluginCall call = new PluginCall(bridge.msgHandler, pluginId, callbackId, methodName, methodData);
bridge.callPluginMethod(pluginId, methodName, call);
} catch (JSONException e) {
Logger.error(e.getMessage());
}
});
bridge.registerInterceptor("js.error", (postData) -> {
Logger.error("JavaScript Error: " + postData.toString());
});
bridge.loadWebView();
return bridge;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/capacitor/src/main/java/com/getcapacitor/Bridge.java` around lines
1512 - 1557, The core interceptors are being attached too late: new Bridge(...)
starts WebView loading before "message" and "js.error" are registered, so early
bridge messages can be ignored. Move the bridge.registerInterceptor(...) setup
for both handlers to happen before any WebView loading starts inside Bridge
initialization, keeping the existing behavior in the Bridge constructor and
MessageHandler flow intact.

Comment on lines +271 to +278
async function replaceFrameworkVariables(config: Config, prefsArray: any[], frameworkString: string): Promise<string> {
prefsArray.map((preference: any) => {
frameworkString = frameworkString.replace(
new RegExp(('$' + preference.$.name).replace('$', '\\$&'), 'g'),
preference.$.default,
);
});
return frameworkString;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Escape preference names before building the replacement RegExp.

Only $ is escaped today; preference names containing regex metacharacters can match the wrong framework text or throw.

Proposed fix
+function escapeRegExp(value: string): string {
+  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
 async function replaceFrameworkVariables(config: Config, prefsArray: any[], frameworkString: string): Promise<string> {
   prefsArray.map((preference: any) => {
     frameworkString = frameworkString.replace(
-      new RegExp(('$' + preference.$.name).replace('$', '\\$&'), 'g'),
+      new RegExp(escapeRegExp(`$${preference.$.name}`), 'g'),
       preference.$.default,
     );
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function replaceFrameworkVariables(config: Config, prefsArray: any[], frameworkString: string): Promise<string> {
prefsArray.map((preference: any) => {
frameworkString = frameworkString.replace(
new RegExp(('$' + preference.$.name).replace('$', '\\$&'), 'g'),
preference.$.default,
);
});
return frameworkString;
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function replaceFrameworkVariables(config: Config, prefsArray: any[], frameworkString: string): Promise<string> {
prefsArray.map((preference: any) => {
frameworkString = frameworkString.replace(
new RegExp(escapeRegExp(`$${preference.$.name}`), 'g'),
preference.$.default,
);
});
return frameworkString;
}
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 273-273: Do not use variable for regular expressions
Context: new RegExp(('$' + preference.$.name).replace('$', '\$&'), 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity. Security best practice.

(regexp-non-literal-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/util/cordova-ios.ts` around lines 271 - 278, Escape the preference
name before constructing the RegExp in replaceFrameworkVariables, since only the
leading $ is currently handled and names with other regex metacharacters can
break matching or replace the wrong text. Update the replacement logic in
replaceFrameworkVariables so it safely escapes the full preference.$.name before
passing it to new RegExp, while keeping the existing default substitution
behavior unchanged.

Source: Linters/SAST tools

Comment on lines 144 to 147
extension InstanceDescriptor {
@objc public var cordovaDeployDisabled: Bool {
return (cordovaConfiguration.settings?["DisableDeploy".lowercased()] as? NSString)?.boolValue ?? false
return false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Don’t leave cordovaDeployDisabled as a stale public value.

CAPInstanceConfiguration still exposes and initializes this property from the descriptor, so hardcoding false breaks callers that read the configuration contract. Either preserve parsing of this flag or remove/deprecate the public configuration property as part of the Cordova extraction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/Capacitor/Capacitor/CAPInstanceDescriptor.swift` around lines 144 - 147,
The public `cordovaDeployDisabled` value in `InstanceDescriptor` is hardcoded
and no longer reflects the configuration contract exposed through
`CAPInstanceConfiguration`. Update the `InstanceDescriptor` property and its
initialization flow so the flag is still parsed and propagated from the
descriptor, or remove/deprecate the corresponding public configuration surface
consistently. Use the `CAPInstanceDescriptor` and `CAPInstanceConfiguration`
symbols to trace the property through the descriptor-to-configuration path and
keep the contract aligned.

Comment on lines +17 to 22
public struct JSCall {
public let options: [String: Any]
public let pluginId: String
public let method: String
public let callbackId: String
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if any code outside Capacitor/Capacitor constructs JSCall directly.
rg -n 'JSCall\(' --type=swift

Repository: Cap-go/capacitor-plus

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n '\bJSCall\b' ios . --type swift --type objc --type h || true
printf '\n--- JS.swift ---\n'
cat -n ios/Capacitor/Capacitor/JS.swift | sed -n '1,120p'

Repository: Cap-go/capacitor-plus

Length of output: 6398


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'CapacitorCordova|JSCall|handleJSCall|public struct JSCall|public init\(' ios . --type swift --type xcodeproj --type plist --type text || true

Repository: Cap-go/capacitor-plus

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '430,470p' ios/Capacitor/Capacitor/CapacitorBridge.swift | cat -n
printf '\n--- JS.swift refs ---\n'
sed -n '1,140p' ios/Capacitor/Capacitor/JS.swift | cat -n

Repository: Cap-go/capacitor-plus

Length of output: 6902


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- CapacitorBridge.swift (around handleJSCall) ---'
sed -n '440,465p' ios/Capacitor/Capacitor/CapacitorBridge.swift | cat -n
printf '\n%s\n' '--- public declarations mentioning JSCall or bridge entrypoints ---'
rg -n 'public .*JSCall|public .*handleJSCall|handleJSCall\(' ios/Capacitor/Capacitor --type swift || true

Repository: Cap-go/capacitor-plus

Length of output: 1953


Make JSCall internal unless you plan to expose it as API. The type is only used by bridge internals, and public still leaves the synthesized initializer non-public, so this widens the surface without giving outside callers a way to create values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/Capacitor/Capacitor/JS.swift` around lines 17 - 22, The JSCall type is
unnecessarily public even though it is only used by bridge internals. Change
JSCall in JS.swift to internal unless you explicitly intend to expose it as part
of the public API, and keep its members aligned with that visibility. If
external access is not required, reduce the type’s access level so the bridge
implementation remains encapsulated.

let method = dict["action"] as? String ?? ""
let callbackId = dict["callbackId"] as? String ?? ""

let args = dict["actionArgs"] as? Array ?? []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify no unspecialized Array casts remain in Swift sources.
rg -n 'as\?\s*Array(\s|$)' ios --glob '*.swift'

Repository: Cap-go/capacitor-plus

Length of output: 286


Specialize the Cordova args cast
Array needs an element type here; cast the Cordova args as [Any] instead of Array.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/CapacitorCordova/CapacitorCordova/Classes/Public/Plugin.swift` at line
43, The Cordova args cast in the plugin argument parsing is too generic; update
the `actionArgs` extraction in `Plugin.swift` to use a concrete `[Any]` type
instead of `Array`, keeping the rest of the `dict["actionArgs"]` handling
unchanged. This applies to the `args` assignment inside the `Plugin` code path
and should preserve the existing default empty array behavior.

Comment thread lerna.json
"main",
"plus"
],
"allowBranch": "next",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'lerna version|allowBranch' .github/workflows/*.yml scripts/*.sh 2>/dev/null

Repository: Cap-go/capacitor-plus

Length of output: 233


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== lerna.json ==\n'
cat -n lerna.json

printf '\n== bump_version workflow excerpt ==\n'
sed -n '1,140p' .github/workflows/bump_version.yml

Repository: Cap-go/capacitor-plus

Length of output: 3215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== branch references ==\n'
rg -n '\bplus\b|\bnext\b|allowBranch|bump_version|lerna version' .github lerna.json package.json . 2>/dev/null || true

Repository: Cap-go/capacitor-plus

Length of output: 50378


lerna.json should allow the plus release branch

npx lerna version runs from .github/workflows/bump_version.yml on plus, so allowBranch: "next" will block automated releases unless plus is added here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lerna.json` at line 7, The release branch guard in lerna.json is too
restrictive because allowBranch only permits next, which blocks the automated
versioning flow on plus. Update the Lerna configuration so the allowBranch
setting includes plus alongside next, keeping npx lerna version compatible with
the bump_version workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.