How to bypass mobile app review thanks to Capacitor, Ionic, and micro frontends 🤯
Tags: javascript, ionic, capacitor, tutorial
Published: 2023-01-23 10:31:51

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](https://reactnative.dev/) or [Flutter](https://flutter.dev/) for instance,
- Hybrid - web app opened in a web view inside of a mobile app - we build web app and then use [Capacitor](https://capacitorjs.com/) 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](https://capacitorjs.com/) at hand.

> To be honest, we have not only Capacitor but also [Cordova](https://cordova.apache.org/) 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](https://ionicframework.com/) I will tell more in a second, I simply recommend Capacitor.
>
> And BTW. People doooon’t like Cordova as [The State of JS 2022](https://2022.stateofjs.com/en-US/libraries/mobile-desktop/) or [StackOverflow Survey](https://survey.stackoverflow.co/2022/#technology-most-loved-dreaded-and-wanted) for the same year suggest so recommending Capacitor instead of Cordova looks reasonable:
>
> ![Almost 3/4 of the people calls Cordova "dreadful" according to 2022 StackOverflow Survey](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0wn0r98sqe0bkn3zp0b4.png)

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”](https://javascript.plainenglish.io/capacitor-turn-your-web-app-into-a-mobile-app-4d114249e55b):

![Capacitor provides a bridge between web app and native functionality](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h13apdlayzlg4nhtc4wn.png)

## 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:

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

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

  console.log('Current position:', coordinates);
};
```

Additionally, as I mentioned, we can help ourselves with [Ionic](https://ionicframework.com/) - 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uy67vzqgtp3bewsiyslf.png)

Toggles:

![Toggles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2rqojerv8bv9jtpbwk4g.png)

And many, many others:

![Buttons, infinity loaders, cards, refreshers, and many, many others...](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/76ob13awj5inirjrhcuy.png)

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](https://create-react-app.dev/). Because of it I want a tool which edits [Webpack’s](https://webpack.js.org/) configuration without [ejecting](https://create-react-app.dev/docs/available-scripts/#npm-run-eject) CRA’s configuration.

Why?

Everything because I will handle our micro frontends using Webpack’s [module federation](https://webpack.js.org/concepts/module-federation/). 

> If you are not familiar with module federation, you can check these videos on YouTube:
- [Micro-Frontends in Just 10 Minutes](https://www.youtube.com/watch?v=s_Fs4AXsTnA)
- [Micro-Frontends Course - Beginner to Expert](https://www.youtube.com/watch?v=lKKsjpH09dU)
- [Five Module Federation/Micro-Frontend Mistakes](https://www.youtube.com/watch?v=0WIFW3s2fDM)
> 

Tool I will use in order to change our Webpack’s configuration is [CRACO](https://craco.js.org/).

## Let’s go 🎉

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

```bash
mkdir ionic-module-federation
cd ionic-module-federation
```

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](https://www.npmjs.com/package/@ionic/cli) and then run `ionic start` for both apps:

```bash
npm install -g @ionic/cli
ionic start
```

Then we get through a wizard for the base app:

```bash
? Use the app creation wizard? No
? Framework: React
? Project name: host
? Starter template: blank
? Create free Ionic account? No
```

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](https://ionic.io/appflow) 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:

```bash
? Use the app creation wizard? No
? Framework: React
? Project name: remote
? Starter template: list
? Create free Ionic account? No
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o2y2rzin3d2c1chty4r3.png)

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](https://ionicframework.com/docs), [Capacitor](https://capacitorjs.com/docs), [Apple](https://developer.apple.com/), and [Google](https://developers.google.com/) 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](https://craco.js.org/). It will let us simply overwrite CRA’s configuration without ejecting.

To start, we can install it for both apps:

```bash
cd host
npm i -D @craco/craco

cd remote
npm i -D @craco/craco
```

> You can use [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces) or [yarn workspaces](https://yarnpkg.com/features/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:

```jsx
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'],
            },
          },
        }),
      ],
    },
  },
};
```

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:

```jsx
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'],
            },
          },
        }),
      ],
    },
  },
};
```

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:

```jsx
import App from 'remote/App';
```

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`:

!["Cannot find module" TS error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jmh6anpf8fncu0vv59xa.png)

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:

```tsx
declare module 'remote/App';
```

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](https://youtu.be/0WIFW3s2fDM?t=1054) 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`:

```tsx
// 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"
},
```

## Then we can run both apps 🎉

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

```bash
cd host
npm start

cd remote
npm start
```

Aaaaand let’s get this error in runtime: 

!["Shared module is not available for eager consumption" error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s83pk2911pajng8wbs06.png)

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:

```tsx
import('./bootstrap');

export {};
```

> You can read more on the topic in “Troubleshooting” section in this article: [Module Federation. Advanced API in Webpack 5.0.0-beta.17](https://blog.devgenius.io/module-federation-advanced-api-inwebpack-5-0-0-beta-17-71cd4d42e534)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tzxs5n8jws1isng8gete.png)

## 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](https://capacitorjs.com/docs/apis/share).

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:  

```tsx
// ...

    <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>

// ...
```

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

```tsx
import { Share } from '@capacitor/share';
```

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

```bash
# ...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
```

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:

```bash
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
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pzkh67zfunjwv1ncjmk7.png)

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

![Our app run inside of the simulator after opening the sharing menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tfjxh27ptnf2bukw0err.png)

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!