DEV Community

Robert Orliński
Robert Orliński

Posted on • Edited on

How to bypass mobile app review thanks to Capacitor, Ionic, and micro frontends 🤯

Disclamer: Tutorial was made based on Ionic 6. In case of issues, please refer to Johan's comment: https://dev.to/maangs/comment/26ji3.

Are you tired of sending your app to Apple or Google to review it every time you update it?

Here I come with the possible solution. Solution because of which you will be able to bypass these reviews 90% of the time or even more.

As you maybe know, we have 4 ways to build mobile apps:

  • Native - one app for iOS and one for Android,
  • Compiled - one codebase compiled to 2 native apps written in React Native or Flutter for instance,
  • Hybrid - web app opened in a web view inside of a mobile app - we build web app and then use Capacitor for example to run this web app inside of a native mobile app,
  • PWA - basically a web app, hosted on the web but behaving like a mobile app (handling offline mode etc.).

And in this text, I want to show how we can use the third approach to make a mobile app which requires app’s review only once while being uploaded to App Store and Google Play and then it doesn’t need it anymore while being updated.

When we update such an app, we can just deploy new code to any server, cloud, static-site hosting provider or any other place which fits for web applications, instead of:

  • building the whole app,
  • uploading it to App Store and Google Play,
  • and then waiting for an approval for the new version.

Let’s see how it can be done!

Let’s start with a bit of theory

I mentioned that in case of hybrid mobile apps, we have Capacitor at hand.

To be honest, we have not only Capacitor but also Cordova which Capacitor is based on but because Capacitor is more popular, has better community, deals with some problems better, and works beautifully with Ionic Framework I will tell more in a second, I simply recommend Capacitor.

And BTW. People doooon’t like Cordova as The State of JS 2022 or StackOverflow Survey for the same year suggest so recommending Capacitor instead of Cordova looks reasonable:

Almost 3/4 of the people calls Cordova

Capacitor is a runtime environment which we can use to open a web app inside of a mobile native app.

Additionally it provides plugins we can use to handle native functionality such as:

  • geolocation,
  • sharing,
  • notifications,
  • accelerometer,
  • and many others.

There is a good sketch that illustrates how Capacitor works I’ve got from the article: “CapacitorJS: Turn Your Web App into a Mobile App”:

Capacitor provides a bridge between web app and native functionality

What does it mean to us?

It means that to make a mobile app using Capacitor, we don’t need anything more than knowledge on how to create a web app!

Dealing with native functionality comes down to using simple JavaScript API. For example:

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

const printCurrentPosition = async () => {
  const coordinates = await Geolocation.getCurrentPosition();

  console.log('Current position:', coordinates);
};
Enter fullscreen mode Exit fullscreen mode

Additionally, as I mentioned, we can help ourselves with Ionic - framework which can be based on React, Vue, or Angular and which provides routing, theming, styles, and most importantly - a lot of built in components made for mobile like:

Alerts:

Alerts

Toggles:

Toggles

And many, many others:

Buttons, infinity loaders, cards, refreshers, and many, many others...

All of them with dedicated styles for iOS and Android.

Getting back to the main topic - if our app is “just a web app”, it can use micro frontends in the same way as in case of web apps!

So we can create 2 separate micro frontends (MFs):

  • first MF that consumes second MF and does nothing else,
  • and the second MF that does everything else.

The first MF will be then built by us and uploaded to App Store and Play Store.

The second MF will be placed under some URL like https://our-awesome-micro-frontend.com and retrieved by the first MF in runtime (every time the user runs our app).

In this scenario, to update our mobile app, we don’t need to build it and upload to App Store and Play Store every time but just update the second, remote MF and deploy it to the server. First MF will retrieve the newest version of the second one in runtime.

But, is it okay for Apple and Google to basically omit their review processes?

I’ve asked myself the same question and the answer is - they are okay with it.

Micro frontends implemented in this way are treated as any other content retrieved from an external API.

Enough theory! Let’s check it by creating our base app and micro frontend it will consume

For code with everything I will show today, you can check my [ionic-module-federation GitHub repository](https://github.com/robert-orlinski/ionic-module-federation)!

I will scaffold both apps using Ionic which by default uses Capacitor (both mentioned before).

I will select React as a used framework but you can select Angular or Vue instead.

If I choose React as a used framework, Ionic’s boilerplate is based on Create React App. Because of it I want a tool which edits Webpack’s configuration without ejecting CRA’s configuration.

Why?

Everything because I will handle our micro frontends using Webpack’s module federation.

If you are not familiar with module federation, you can check these videos on YouTube:

Tool I will use in order to change our Webpack’s configuration is CRACO.

Let’s go 🎉

We can create a new catalogue (let’s call it ionic-module-federation):

mkdir ionic-module-federation
cd ionic-module-federation
Enter fullscreen mode Exit fullscreen mode

Inside of it we create 2 new projects:

  • 1 for our base app that will be uploaded to App Store and Google Play,
  • and 1 for micro frontend that will be consumed by the base app.

To crate them, we can install @ionic/cli and then run ionic start for both apps:

npm install -g @ionic/cli
ionic start
Enter fullscreen mode Exit fullscreen mode

Then we get through a wizard for the base app:

? Use the app creation wizard? No
? Framework: React
? Project name: host
? Starter template: blank
? Create free Ionic account? No
Enter fullscreen mode Exit fullscreen mode

Going through the options I’ve chosen:

  • I don’t want to use app creation wizard. If I did, I would be redirected to Ionic’s website, need to create an account, and my app would be automatically added to Appflow’s account. I don’t need these.
  • I choose React as my JS framework.
  • host is the name of my base app.
  • I don’t want any starter template for my base app so I choose blank.
  • And I don’t want to create Ionic’s account as I mentioned in the first point.

And then do the same for the micro frontend consumed by our base app:

? Use the app creation wizard? No
? Framework: React
? Project name: remote
? Starter template: list
? Create free Ionic account? No
Enter fullscreen mode Exit fullscreen mode

The only differences here are:

  • The name (of course).
  • The fact that I am using starter template. Probably it’s not required in your case but to nicely show how our base app consumes the remote app I will use it.

Then we land with our 2 front-end projects

File structure looks like this:

Very similar to Create React App. Contains additional Capacitor and Ionic configs

Both of them are based on Create React App.

Of course, right now we can develop our apps in a way Ionic and Capacitor let us to develop them:

  • create web apps,
  • handle native functionality in places we need to by using Capacitor’s plugins,
  • build our apps for iOS and Android,
  • test them using Xcode and Android Studio,
  • upload them to App Store and Google Play.

But these actions are not the ones I will take right now. You can read more on them in Ionic, Capacitor, Apple, and Google documentations.

Right now, I want to do micro frontend stuff! 🥳

So let’s configure module federation for both apps!

As I mentioned, in our case, the perfect tool for this job is CRACO. It will let us simply overwrite CRA’s configuration without ejecting.

To start, we can install it for both apps:

cd host
npm i -D @craco/craco

cd remote
npm i -D @craco/craco
Enter fullscreen mode Exit fullscreen mode

You can use npm workspaces or yarn workspaces to install it once.

And then create a file in which our configuration will be placed. File has to be called craco.config.js.

We will create it for both apps - host and remote.

For host our craco.config.js will have such a content:

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;

module.exports = {
  webpack: {
    configure: {
      output: {
        publicPath: 'auto',
      },
    },
    plugins: {
      add: [
        new ModuleFederationPlugin({
          name: 'host',
          remotes: {
            remote: 'remote@http://localhost:3002/remoteEntry.js',
          },
          exposes: {},
          filename: 'remoteEntry.js',
          shared: {
            ...deps,
            react: {
              singleton: true,
              eager: true,
              requiredVersion: deps['react'],
            },
            'react-dom': {
              singleton: true,
              eager: true,
              requiredVersion: deps['react-dom'],
            },
          },
        }),
      ],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Where http://localhost:3002 is the address of our remote app.

In case you upload remote app to some remote server, you will need to replace it by server’s url.

FYI - to always open remote app on port 3002 instead of default 3000 I change start script inside of remote's package.json from react-scripts start to PORT=3002 react-scripts.

In turn, for remote app craco.config.js will have this content:

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;

module.exports = {
  webpack: {
    configure: {
      output: {
        publicPath: 'auto',
      },
    },
    plugins: {
      add: [
        new ModuleFederationPlugin({
          name: 'remote',
          remotes: {},
          exposes: {
            './App': './src/App',
          },
          filename: 'remoteEntry.js',
          shared: {
            ...deps,
            react: {
              singleton: true,
              eager: true,
              requiredVersion: deps['react'],
            },
            'react-dom': {
              singleton: true,
              eager: true,
              requiredVersion: deps['react-dom'],
            },
          },
        }),
      ],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Very similar. The only difference is the fact that instead of setting remotes in module federation configuration, we expose specific components.

Then we can use our exposed component

In my case, I expose only one component. This is the main one (App) because I want to consume whole remote app basically.

Then to consume this component, we can simply import it inside of the host app in this way:

import App from 'remote/App';
Enter fullscreen mode Exit fullscreen mode

I will do it inside of the host/index.tsx file.

BTW. Regarding .tsx files - Ionic supports TypeScript by default and there is even no way to generate boilerplate with files written in classic JS.

Ionic suggests that if for some reason you don’t want to use TS, you can simply rename all files and remove type annotations from them.

Probably you’ve already spotted that TypeScript gives an error when we import our App:

That’s because module federation doesn’t contain any types.

I can silence TypeScript for now by for example creating file called remote.d.ts inside of our src directory and put code like this inside:

declare module 'remote/App';
Enter fullscreen mode Exit fullscreen mode

but of course, this is not a good solution.

You can find better solution in the 5th part of Five Module Federation/Micro-Frontend Mistakes video I’ve mentioned several paragraphs before.

Now we can run our app!

But one more thing.

To use CRACO, we need to change start, build, and test commands in package.json files for both host and remote:

// host:

"scripts": {
  "start": "PORT=3002 craco start",
  "build": "craco build",
  "test": "craco test --transformIgnorePatterns 'node_modules/(?!(@ionic/react|@ionic/react-router|@ionic/core|@stencil/core|ionicons)/)'",
  "eject": "react-scripts eject"
},

// remote: 

"scripts": {
  "start": "craco start",
  "build": "craco build",
  "test": "craco test --transformIgnorePatterns 'node_modules/(?!(@ionic/react|@ionic/react-router|@ionic/core|@stencil/core|ionicons)/)'",
  "eject": "react-scripts eject"
},
Enter fullscreen mode Exit fullscreen mode

Then we can run both apps 🎉

Let's use npm start twice - inside of a host and remote app separately:

cd host
npm start

cd remote
npm start
Enter fullscreen mode Exit fullscreen mode

Aaaaand let’s get this error in runtime:

Ouch 🤧

Fortunately, there is nothing to worry about.

We only need to create new file (let’s call it src/bootstrap.tsx as it was adapted), copy src/index.tsx content into it and change already copied src/index.tsx content to:

import('./bootstrap');

export {};
Enter fullscreen mode Exit fullscreen mode

You can read more on the topic in “Troubleshooting” section in this article: Module Federation. Advanced API in Webpack 5.0.0-beta.17

When we restart our apps now.

We should see the same content under both [localhost:3000](http://localhost:3000/) (host) and [localhost:3002](http://localhost:3002/) (remote):

Our app run inside of the browser

That means, our module federation works! 🥳

We can change anything inside of remote/App component and see that the change is visible both on [localhost:3000](http://localhost:3000/) and [localhost:3002](http://localhost:3002/).

And that also means we will be able to skip mobile app’s review after we upload our app to App Store and Google Play!

We don’t have to edit anything inside of our host micro frontend (one which will be then uploaded to App Store and Google Play) to update our mobile app.

We can always update only remote micro frontend (the same one we can host “somewhere on the internet”) and just retrieve it in runtime.

With one note - we can do it as long as we don’t need to add any native functionality to our mobile app

If we want to handle stuff like geolocation, notifications, or accelerometer in remote, we have to install proper Capacitor plugin both in host and remote and then sync native apps.

We can do it to show how it works.

Let’s test sharing native functionality.

First of all, we can add some code in our remote app that will handle content sharing.

I will add very simple button to the end of the Home component, that will invoke sharing on click:

// ...

    <IonList>
      {messages.map((m) => (
        <MessageListItem key={m.id} message={m} />
      ))}
    </IonList>
  </IonContent>

  <IonButton
    onClick={async () => {
      await Share.share({
        title: 'See cool stuff',
    text: 'Really awesome thing you need to see right meow',
    url: 'http://ionicframework.com/',
    dialogTitle: 'Share with buddies',
      });
    }}
  >
    Share 
  </IonButton>
</IonPage>

// ...
Enter fullscreen mode Exit fullscreen mode

Share is imported from the @capacitor/share plugin which handles sharing functionality:

import { Share } from '@capacitor/share';
Enter fullscreen mode Exit fullscreen mode

Of course, our @capacitor/share plugin has to be installed:

# ...in `host` app to take it into consideration while updating native platforms:

cd host
npm install @capacitor/share

# ...and in `remote` app to import it properly and without errors:

cd remote
npm install @capacitor/share
Enter fullscreen mode Exit fullscreen mode

Then I can build our host app and generate it for native platforms.

For testing purposes, I will add only iOS platform to our Ionic project, then generate and open our app inside of XCode:

cd remote 
npm start # If we want to run our `host` app after building it and consume our micro frontend, `remote` app has to operate.

cd host
ionic capacitor add ios
npm run build
ionic capacitor sync --no-build # `ionic capacitor sync` builds our app using `react-scripts` so because I use CRACO, I don't use this default build to happen. I build our app using `npm run build` before running `ionic capacitor sync`.
ionic capacitor open ios
Enter fullscreen mode Exit fullscreen mode

After running ionic capacitor open ios, native app gets opened inside of XCode. When we run it, we see our button at the bottom of the home view:

Our app run inside of the simulator

Then when we click on it, we can see our sharing menu:

Our app run inside of the simulator after opening the sharing menu

It wouldn’t work if we added @capacitor/share package only to the remote app. We had to add it also to our main app - that is the host app.

And that would be it!

Once more, you can check [ionic-module-federation repo](https://github.com/robert-orlinski/ionic-module-federation) for code created in this tutorial.

In the way I’ve shown, we can deploy new version of our mobile app anytime we want:

  • Without asking Apple and Google for review.
  • Without building and uploading our app to App Store and Google Play every time.

We can just have 1 app which consumes remotely hosted micro frontend. That micro frontend can be changed anytime we want (assuming that we don’t add any new native functionality).

I hope this tutorial was useful for you!

Top comments (5)

Collapse
 
maangs profile image
Johan Magnusson

Great article Robert, informative and easy to follow!

I would like to point out some things that could be helpful to others:

  • I had to downgrade ionic/CLI to v6 for this to work out of the box. v7 seems to be using vite as default instead of webpack.
  • start script for host and remote should be swapped. (remote should be served at port 3002 and not the other way around)
  • If you are using a windows machine you'll get an error when you try to start the remote app. Solution for this is to replace start script with "start": "set PORT=3002&& craco start",
Collapse
 
robertorlinski profile image
Robert Orliński

Thank you Johan for these words and especially - for listing these 3 points for others!

Collapse
 
zakuru profile image
Zakuru San

Hi Robert,
Great article thanks for sharing.
Relying on module federation this way would indeed bypass the need to go through the store review process but then your JS/CSS assets are being fetched remotely which will impact the app performance because it has to wait for the assets to be downloaded.
Am I correct ?

Collapse
 
robertorlinski profile image
Robert Orliński

Hello! đź‘‹

Thank you for these words! I appreciate it.

But you are right and this is a good point - by default all resoruces are downloaded during the app’a opening.

You can try to overcome it by using some hybrid approach in which you update the app itself by sending it to Apple/Google and use microfrontends conditionally (if there is newer microfrontend you use it, if already approved app’s version is the latest, you use it instead).

Additionally, I am not sure how does the webview used under the hood by Capacitor handles “browser” cache. Maybe this is something we can rely on.

This is for sure a tradeoff we have to have in our minds. Thank you for rising it 🙏

Collapse
 
testengine999 profile image
Jay Developer

I got Error "Uncaught (in promise) Error: Cannot find module 'remote/App'"