Migrating from Cordova to Capacitor

I’m not in the know enough here to maybe understand the why.

But it’s seems rendering an iOS WebView natively or the Meteor React Native support package would be preferable now compared to when Meteor originally was trying to do Cordova support?

Capacitor is rendering an iOS WebView natively/directly. I think it’s about as efficient and performant as you can get while still getting to render HTML.

React Native is great but it requires you to rewrite your entire view layer in React Native components, as it doesn’t render DOM HTML but native components instead, so it’s not a replacement (nor an option for this one-man development team that has to maintain a web version of the app as well).

3 Likes

Amazing initiative @banjerluke
Please, take a look in our paid articles program, this journey gives a good content :wink:

Hello everyone,

Thanks a lot @banjerluke for your post — it really helped me get back into trying to publish a Meteor app to the mobile stores! Also, big thanks to @nachocodoner for your reply to my previous post on this topic.

I’m almost there. I’ve managed to set up a relatively comfortable dev environment with this Makefile, which automatically synchronizes the Meteor Cordova build with the Capacitor mobile build:

build-sync:
	rm -rf capacitor/www-dist/* || exit 1; \
	cp .meteor/local/build/programs/web.cordova/*.{css,html,json} capacitor/www-dist || exit 1; \
	cp -r .meteor/local/build/programs/web.cordova/packages capacitor/www-dist/ || exit 1; \
	mkdir capacitor/www-dist/app || exit 1; \
	cp -r .meteor/local/build/programs/web.cordova/app/*.js capacitor/www-dist/app || exit 1; \
	rsync -r --exclude='*.js' .meteor/local/build/programs/web.cordova/app/ capacitor/www-dist/ || exit 1; \
	rm -rf capacitor/www-dist/**/*.map || exit 1; \
	sed -i '' "s|WebAppLocalServer\.onError(function(e){console\.error(e)})||g" capacitor/www-dist/**/*.js || exit 1; \
	curl -f --connect-timeout 20 'http://localhost:3000/__cordova/index.html' | \
		sed -e 's|/__cordova/client/|/client/|g' -e 's|/__cordova/|/|g' \
		> capacitor/www-dist/index.html; \
	sed -i '' "3r capacitor/WALS-shim.html" capacitor/www-dist/index.html; \
	npx cap sync;

enable-watch:
	watchman-make -p '.meteor/local/build/programs/web.browser/**/*' -t build-sync

Combined with @nachocodoner’s suggestion in the Capacitor config:

"server": {
  "url": "http://{{LOCAL_IP}}:{{PORT}}",
  "cleartext": true
}

Now, I’ve hit what might be the final hurdle.
I’m working with Meteor 3.3.2, and I’m facing the same issue I ran into in previous experiments.

Our project uses the npm dependency @tanstack/react-query@5.36.0. Everything works perfectly on a classic web session - or using the development capacitor config since it directly loads the url within the WebView, but something breaks in the Cordova build:

getQueryDefaults(queryKey) {
  const defaults = [...__privateGet(this, _queryDefaults).values()];
  // TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function
  const result = {};
  defaults.forEach((queryDefault) => {
    if ((0, import_utils.partialMatchKey)(queryKey, queryDefault.queryKey)) {
      Object.assign(result, queryDefault.defaultOptions);
    }
  });
  return result;
}

I believe that same error was raised when using the web.browser.legacy build.

After digging into the generated builds, I noticed that the packages/modules output differs between builds.
That’s confusing, since according to the Meteor documentation, Cordova bundles should now be modern.

Has anyone else encountered this, or figured out how to make sure the Cordova build uses the same modern compilation as web.browser?

Any pointers appreciated!

Huh. I had this in my meteor-settings.json:

  "packages": {
    "modern-browsers": {
      "unknownBrowsersAssumedModern": true
    }
  },

The link in your post suggests this is no longer needed with Meteor 3.3.2+, but could be worth a try.

You could also try putting this in your package.json:

  "meteor":
    "nodeModules": {
      "recompile": {
        "@tanstack/react-query": "legacy",
      }
    }
  },
2 Likes

Thanks @banjerluke for your reply. I already tried that suggestion, but it didn’t fix the issue.

After inspecting Meteor’s core more deeply, I’m convinced the root of this problem is that web.cordova is still being compiled under “legacy” assumptions, which is incompatible with many modern JS packages. To solve that properly, we need to treat the Cordova (and Capacitor) build paths as modern by default – just as already suggested in documentation.

I’ll open a draft PR to propose that change. Here is the draft PR Consider `web.cordova` as a modern architecture by tmeyer24 · Pull Request #13983 · meteor/meteor · GitHub

@nachocodoner, I believe this will be essential for proper Capacitor integration as well. It doesn’t make sense that @tanstack/react-query is the only package failing under legacy build—more likely there are multiple packages that assume modern JS semantics. On mobile, just as on the web, usage of modern JavaScript is now the norm. Libraries’ expectations have shifted, and we need the build system to keep pace.

2 Likes

Does this support social sign-ins?

I also know there could be some challenges with getting the sessions signed into the browser to load up as part of the authentication.

Does this support social sign-ins?

I don’t see why it wouldn’t. I’m guessing you’d have to use @capacitor/browser or another in-app browser plugin to show the OAuth screens, but that’s probably the same for Cordova right? It’s possible you’d also need to configure deep links for the app to capture OAuth callbacks. Just guessing here; I don’t use social sign-ins in my app, although I do use an in-app browser for other things and I have deep links set up.

Yeah I think there can be an issue where it loads up a clean browser window with no session information, meaning user has to sign into Google/Facebook/etc even if their mobile browser is already signed in. I believe the Meteor-Cordova integration magically handles that, would be great to see that here too if you have plans to make it a Meteor package or merge into Meteor Core.

As far as I’m aware, the only runtime “special sauce” that Meteor adds to the Cordova app is cordova-plugin-meteor-webapp. I intend to replicate all relevant functionality from that plugin, except for the bundled web server (since Capacitor handles that now) and local filesystem access (since it’s no longer trivial given that we’re using Capacitor’s web server, and I think we’re better off leaving that to other plugins). I’ve completed the reimplementation for iOS (pending further testing) and plan to tackle Android soon.

As far as I can tell, cordova-plugin-meteor-webapp doesn’t add any “magic” in terms of sharing session with the system browser, however. I don’t think that’s even possible unless you actually open the system browser, i.e. the OS switches over to Safari or Chrome (or whatever the default browser is) and away from your app. Besides, as far as I know you can’t even open in-app browser windows for the auth in Cordova unless you have one of the plugins; it’s not part of cordova-plugin-meteor-webapp’s API:

declare global {
  interface Window {
    WebAppLocalServer: {
      startupDidComplete(callback?: () => void): void;
      checkForUpdates(callback?: () => void): void;
      onNewVersionReady(callback: (version: string) => void): void;
      switchToPendingVersion(callback?: () => void, errorCallback?: (error: Error) => void): void;
      onError(callback: (error: Error) => void): void;
      localFileSystemUrl(fileUrl: string): never;
    };
  }
}
1 Like

I pushed a bunch of updates to my cordova-plugin-meteor-webapp replacement plugin, capacitor-meteor-webapp.

It’s got Android support now, along with a whole suite of tests for both platforms (and bug fixes to go along with them). I’m feeling a lot more confident in its production-readiness and am going to update my public beta apps (used by a couple hundred folks) to Capacitor very soon.

To be clear: AI agents (Opus 4.6 & Codex 5.3) did all of the coding, though I was heavily involved in the (multiple) planning and review stages. I think the result is better than if I’d done it myself (given that this is out of my wheelhouse as a primarily-web programmer) but I want to be sure to disclose that. I did everything I could to make a robust plugin because the last thing I want is to have thousands of broken app installs on my production app – that would be catastrophic. (Writing that makes me a bit nervous… but that’s why I have a beta version!)

Now to finally integrate RevenueCat! :smiley_cat:

4 Likes

Nice stuff!

OMG yes hahaha

How’s the RevenueCat integration going?

Have not gotten to that point yet. Had to prioritize some other things in the backlog. So far so good with Capacitor, though… big public test (probably ~200 people) is coming soon, but internally there have been no issues with the TestFlight version.

1 Like

Quick update: released a public beta version of my app about ten days ago, powered by Meteor 3.2 (yes I need to update), Capacitor 8, and my capacitor-meteor-webapp plugin, which is now in use by ~100 iOS users. Multiple hot-code pushes since then. Zero issues logged or reported. Feeling good about it. Going to try publishing an Android beta and getting some Android users using it next.

re: RevenueCat @msavin - still haven’t worked on actually integrating the SDK with my codebase, but I got it installed and bundled in the Xcode project. Simple as meteor npm install @revenuecat/purchases-capacitor.

2 Likes

My Meteor-powered Capacitor app (or is it Capacitor-powered Meteor app?) is out of beta on both the Apple App Store and Google Play Store! It will take time for the thousands of users to be updated to the app, but so far there have been zero issues with cross-compatibility. I’m super chuffed with how this all turned out. My @banjerluke/capacitor-meteor-webapp plugin needed no further updates; I’ll probably take off the “WORK IN PROGRESS” scare copy soon.

Oh, and RevenueCat is onboard as well! (In parallel with Iaptic Billing while I complete the transition.)

4 Likes

So everything has been working great… except for one wrinkle that I failed to predict:

Every user found themselves logged out.

Of course, this makes sense. Capacitor and Cordova are using different internal domains, so they have different cookies/localStorage databases. With hindsight, perhaps I could have figured out some way to migrate sessions from Cordova to Capacitor, perhaps by storing the login token in the app “preferences” or somewhere on the filesystem (with an update that I put out well in advance using Cordova) and then fetching that after updating to the new Capacitor-powered app. It’s tricky, though. For some apps, it might not matter, but I have users who have been logged in for years and no longer have access to the email they signed up with, so we had to help them out manually. Plus, some users opened the app where they had no internet connection expecting to be able to use it, but since a background update had moved them over to the new Capacitor app, they found themselves logged out when they opened it up.

Anyway, just wanted to give that heads-up to anyone else considering a Cordova-to-Capacitor migration. FWIW, there have been no other observed or reported issues related to this migration across thousands of app installs and many live Meteor updates as well as several App Store updates.

3 Likes

Congrats. Too bad about the logout wrinkle. Still, quite an achievement if that’s the only significant issue users face (though agreed it’s a big one). Good luck helping them out.

If you have the cycles, consider writing this one up :slight_smile: It would be so helpful to others modernizing their Cordova apps.

1 Like

How did you achieve your offline capability in your Capacitor app?

Those unexpected turns with forced logouts etc are part of the learning I guess. Did you migrate them in batches or was this only becoming an issue when you mass migrated them?

Still, congrats on moving them off Cordova and onto Capacitor. What advantages do you users realize on Capacitor or are the only benefits on your end?

Offline capability is through IndexedDB via Dexie and my GroundedCollection wrapper which I posted and wrote about awhile back. (Though I’m in the process of adding @capawesome-team/capacitor-sqlite as a backend for iOS to work around a long-standing WebKit bug with IndexedDB.) No changes from what I was doing in Cordova, which also mirrors what I’m doing in the browser and ToDesktop except I also need a service worker there (powered by Workbox).

Logouts: basically, I released the app to the App Store / Play Store, then as users slowly started to get updated to the latest Capacitor-powered shell they found themselves logged out. So it was a trickle at first, then a river, and now seems to be calming down as most people have been updated at this point. It’s my fault for not properly testing the production deployment using TestFlight. (I did plenty of testing, but on a different TestFlight-only bundle ID, which made me complacent.)

Benefits to users are mostly that I can more easily maintain and improve the software in the future due to more access to modern plugins and build tooling. But I also think that startup times have improved, though I didn’t do any benchmarking.

Pretty sure I’ve written everything someone (or their AI agent) would need to get up and running in the README/documentation for the plugin I made, which includes a sample build script as well. Happy to answer questions if anyone finds gaps in that documentation while attempting their own migration. I suspect some aspects of it may change with upcoming Meteor versions in the not-too-distant future. Also, caveat that I’m using meteor-vite for builds; I suspect an Rspack-powered app would work even smoother but I’m not sure as I can’t use Rspack and have Svelte hot-reload.

2 Likes