In the following I will show how it is possible to have two app’s from one React Native Expo repository with a little help from Python and a bit of Powershell.

While building an app for a startup I am currently working on, I ran into the issue of having to support two different apps from the same repository. In our case it is a matter of having one app for "professionals" and one app for "patients". A lot of the screens are exactly the same for either app and in order to foster re-use and not having to create a whole new repo as a copy of the other, I wanted to be able to start and build two individual apps from the same repository.

This proved to be quite difficult.

I may have been bad at searching or explaining when I asked for help on reddit etc. But the bottom line was that I was unable to find someone who could tell my how it could be done. So with the uneasy feeling of actually having to do something myself, I started experimenting to see whether it could be done. (Spoiler alert: It could)

First part of it has to do with app.json. My theory which ended up being correct, was that by changing "name" and "slug" then it was possible to actually have two different apps build and released, first of all to Expo. The following two app.json files will create two independent apps in Expo, even though they are built from the exact same code base:

App 1

{
  "name": "App 1",
  "expo": {
    "sdkVersion": "33.0.0",
    "name": "App 1",
    "slug": "app1",
    "version": "0.0.22",
    "description": "",
    "privacy": "unlisted",
    "orientation": "portrait",
    "icon": "./src/icons/App1_iOS_1024.png",
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "me.identifier.app1"
    }
  }
}

App 2

{
  "name": "App 2",
  "expo": {
    "sdkVersion": "33.0.0",
    "name": "App 2",
    "slug": "app2",
    "version": "0.0.22",
    "description": "",
    "privacy": "unlisted",
    "orientation": "portrait",
    "icon": "./src/icons/App2_iOS_1024.png",
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "me.identifier.app2"
    }
  }
}

Since I have included the "ios" settings I might as well give the spoiler; that not only does this work when publishing to Expo, it is possible to build for iOS and have individual separate apps in the AppStore as well.

Separation in code

Now that I proved that we can build two individual apps, then we need to separate the entry/startpage of the apps. At least that was the scenario I needed. I had to present one type of user with a specific startpage for their needs and another for the other type. This stretched for both on-boarding and later running of the two "separate" apps. Each type needed their distinct description of how to use the app etc.

In my React Native app I am using react-navigation and needed a way to present different startup screen depending on whether I started the "patient" app or the "professional" app.

This Stack Navigator works in a very simple way in that the first screen configured will be used as the startup screen. Therefore the simple way to control whether the user starts in the flow of the "patient" or the "professional" is to have either screen appear as the first one. So commenting out "patient" screen will give "professional" app and uncommenting will give "patient" app.

I have since speculated whether this was the best way to solve it as there, as far as I remember, also is an "initialStartupScreen" setting or something like it. But this worked and will be 100% handled in code (explained later), so I am not that worried for now.

export default createAppContainer(
  createStackNavigator(
    {
//      UserTypeLoginScreen: { screen: UserTypeLoginScreen },
      Main: {
        screen: mainBottomNavigation,
        navigationOptions: ({ navigation }) => ({
          header: null
        })
      },

This could be handled manually, but would be annoying and error prone to run through a number of manual steps and comment/uncomment different lines of code each time the other type of app needed to be build or run. Hence I build a small python script to take care of the AppNavigation.js file and make sure that depending on the app being build/run it will comment/uncomment the line that controls which screen to present at startup. It also comments/uncomments the import of that screen as well.

import shutil
import os
import sys
# Get type of app - which one are we trying to build/run
appType = sys.argv[1]

# Remove .expo as it can cause problems and is easily re-created
shutil.rmtree('./.expo')

# Create temporary file
file = "./src/helpers/AppNavigation.js"
shutil.move(file, file+"~")

# Not the prettiest but it works
# Find lines in AppNavigation.js that mentions "UserTypeLoginScreen"
# Comment them if they are not commented and should be, given the "appType"
# so we do not double-comment if the same type is started twice
destination = open(file, "w")
source = open(file+"~", "r")
for line in source:
    if "UserTypeLoginScreen" in line:
        if "patient" in appType:
            line = line.replace("//", "")
        else:
            if not "//" in line:
                line = "//" + line
    destination.write(line)
source.close()

destination.close()

os.remove(file+"~")

So just to quickly walk you through it. First of all it looks for an argument to decide whether we are currently trying to start the "patient" or "professional" app. I will later show how this script is called.

Then it removes the .expo folder as it sometimes gave issues when switching from running one app to the other. And as it only takes a few seconds to re-create I just remove it here and have it be re-created.

Then it creates a temporary file from the AppNavigation.js file and comments/un-comments the correct lines and finally removes the temporary file.

Putting it all together

Now finally I needed to create some small scripts to link it all together and be able to start or build either app. I decided to create them as Powershell scripts and decided on 4 scripts for now:

  • Patient run
  • Patient build iOS
  • Professional run
  • Professional build iOS

Here is an example of "Patient run" script:

copy-item -path ./src/app_type/patient/app.json -destination .

python ./app_prepare.py patient

npm start

First it copies the app.json file to the same directory as this Powershell script is located as it is located along side the app.json file that is being used to run the app.

Then it calls the python script that we created above. It call it with "patient" as parameter. This is then used as "appType" in the python script.

And finally it calls npm start. This is defined to run "expo start -c" in the package.json file.

If we then look at the Powershell script for building iOS .ipa package:

copy-item -path ./src/app_type/patient/app.json -destination .

python ./app_prepare.py patient

expo build:ios

The first two lines are exactly the same, the only difference is that in the last line it calls "expo build:ios" instead.

I will not include them here, but of course for the "professional" app it just copies the app.json file from the "professional" directory where it is stored and calls the python script with "professional" instead.

This then enables me to run or build the app I want by just calling the corresponding Powershell script. And voila – 2 apps from the same repository.

So finally just a few pros and cons as this of course may not be the right solution for anyone:

Pros

  • Re-use of components

Cons

  • Bigger release packages as they can carry unsused screens

Feel free to get in touch if you have any questions to the above or if you have any ideas to optimize it even further.