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 plugin | Capacitor replacement | What actually changes |
|---|---|---|
| cordova-plugin-camera | @capacitor/camera | Promise-based instead of callback-based; iOS Info.plist key is unchanged |
| cordova-plugin-geolocation | @capacitor/geolocation | watchPosition id is now a string, not a number |
| cordova-plugin-splashscreen | @capacitor/splash-screen | Config moves from <preference> tags into capacitor.config.ts |
| cordova-plugin-statusbar | @capacitor/status-bar | Style is now a named enum, not a boolean flag |
| cordova-plugin-local-notification | @capacitor/local-notifications | Scheduling uses a trigger object instead of a bare at Date |
| cordova-plugin-network-information | @capacitor/network | Connection.* 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'splatforms/androidandplatforms/ios, with whatever you hand-edited inAndroidManifest.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 oldplatforms/tree before you delete it, or it's gone. - Hooks have no direct equivalent.
config.xml<hook>entries (before_build,after_prepareshell scripts) need porting to either a Capacitor plugin's own native lifecycle, or a plain npmpostbuildscript. - Plugin variables disappear. Cordova's
<preference name="...">values, passed as--variableat install time, have no equivalent install-time mechanism -- those values move intocapacitor.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;
a migration order that doesn't break the build halfway through
- Add Capacitor alongside your existing Cordova dependencies (
ionic integrations enable capacitor) without removing anything yet -- both can coexist briefly. - 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.
- Once every plugin is swapped and both native projects build clean from
android/andios/, removeplatforms/and the remainingcordova-*dependencies. - Delete
config.xmllast, only once every setting it held has a confirmed home incapacitor.config.tsor 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.
