Migrating an old Ionic/Angular app off deprecated Cordova plugins to Capacitor



If your Ionic/Angular app still has cordova-plugin-* packages in its lockfile, that's usually the thing now blocking an Xcode or Android target SDK bump -- most of that ecosystem has been effectively unmaintained since 2021-2022, and Capacitor has been Ionic's default runtime since v6. The good news: the plugins that show up in almost every old Ionic app -- camera, geolocation, splash screen, status bar, local notifications, network status -- all have direct, actively-maintained Capacitor replacements. The migration is mechanical for most of it. A few spots genuinely break.

plugin-by-plugin mapping

Cordova pluginCapacitor replacementWhat actually changes
cordova-plugin-camera@capacitor/cameraPromise-based instead of callback-based; iOS Info.plist key is unchanged
cordova-plugin-geolocation@capacitor/geolocationwatchPosition id is now a string, not a number
cordova-plugin-splashscreen@capacitor/splash-screenConfig moves from <preference> tags into capacitor.config.ts
cordova-plugin-statusbar@capacitor/status-barStyle is now a named enum, not a boolean flag
cordova-plugin-local-notification@capacitor/local-notificationsScheduling uses a trigger object instead of a bare at Date
cordova-plugin-network-information@capacitor/networkConnection.* constants gone; connection type is a string union

cordova-plugin-camera → @capacitor/camera

Before:

navigator.camera.getPicture(
  (imageData) => { this.photo = 'data:image/jpeg;base64,' + imageData; },
  (err) => { console.error(err); },
  { quality: 75, destinationType: Camera.DestinationType.DATA_URL }
);

After:

import { Camera, CameraResultType } from '@capacitor/camera';

const photo = await Camera.getPhoto({
  quality: 75,
  resultType: CameraResultType.Base64,
});
this.photo = `data:image/jpeg;base64,${photo.base64String}`;

The breakage point here isn't the API shape, it's Android. If your platforms/android/AndroidManifest.xml had hand-added permission or FileProvider entries, those lived in a folder Cordova regenerated on every build and Capacitor doesn't touch the same way -- you need to re-add them to the real, permanent android/app/src/main/AndroidManifest.xml that now lives in your repo.

cordova-plugin-geolocation → @capacitor/geolocation

Before:

navigator.geolocation.watchPosition(
  (pos) => this.updatePosition(pos.coords),
  (err) => console.error(err),
  { enableHighAccuracy: true }
);

After:

import { Geolocation } from '@capacitor/geolocation';

const watchId = await Geolocation.watchPosition(
  { enableHighAccuracy: true },
  (position, err) => {
    if (position) this.updatePosition(position.coords);
  }
);
// later
await Geolocation.clearWatch({ id: watchId });

The breakage point: watchId is a string now, not a number. Code that stored it in a number[], or used it as a numeric map key, fails silently in a project without strict TypeScript -- which a lot of old Ionic apps are.

the common breakage points, beyond any one plugin

  • Callbacks become promises. Anything that assumed a synchronous-feeling callback inside an Angular template binding needs a second look at change detection -- native bridge responses sometimes land outside Angular's zone, and a value that silently never updates the UI is usually a missing NgZone.run(), not a broken plugin.
  • The platforms/ folder disappears. Cordova's platforms/android and platforms/ios, with whatever you hand-edited in AndroidManifest.xml, Info.plist, or the Podfile, don't exist in Capacitor's model. The native projects (android/, ios/) are checked into your repo and edited permanently -- find every hand edit in the old platforms/ tree before you delete it, or it's gone.
  • Hooks have no direct equivalent. config.xml <hook> entries (before_build, after_prepare shell scripts) need porting to either a Capacitor plugin's own native lifecycle, or a plain npm postbuild script.
  • Plugin variables disappear. Cordova's <preference name="..."> values, passed as --variable at install time, have no equivalent install-time mechanism -- those values move into capacitor.config.ts.

moving config.xml into capacitor.config.ts

Before:

<preference name="SplashScreenDelay" value="3000" />
<preference name="StatusBarStyle" value="lightcontent" />
<allow-navigation href="https://api.myapp.com/*" />

After:

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.codeceibe.myapp',
  appName: 'MyApp',
  webDir: 'www',
  server: {
    allowNavigation: ['api.myapp.com'],
  },
  plugins: {
    SplashScreen: {
      launchShowDuration: 3000,
    },
    StatusBar: {
      style: 'DARK',
    },
  },
};

export default config;
Cordova's regenerated platform folders versus Capacitor's checked-in native projectsCordovaWebView + cordova.js bridgeplugins/ (config.xml references)platforms/android, platforms/iosregenerated on everycordova prepare -- edits get lostCapacitorWebView + native bridgenpm plugin packages call native codeandroid/, ios/ checked into the repoedited directly, kept permanently,never regenerated

a migration order that doesn't break the build halfway through

  1. Add Capacitor alongside your existing Cordova dependencies (ionic integrations enable capacitor) without removing anything yet -- both can coexist briefly.
  2. Swap plugins one at a time, starting with the least-used one in the app, running a real device build after each swap rather than batching them.
  3. Once every plugin is swapped and both native projects build clean from android/ and ios/, remove platforms/ and the remaining cordova-* dependencies.
  4. Delete config.xml last, only once every setting it held has a confirmed home in capacitor.config.ts or the native project files directly.

Doing it in that order means a failed step leaves you with a working app on the old stack, not a half-migrated one that builds for neither platform.

Leave a comment

Anonymous comments are published after review.