How can I install my preview and development builds simultaneously?

First off, I know I could go through manually and change the package name between compiles. If I have to do that I will, but I’m hoping there’s a better way to do this.

I’m using EAS build and basically I’d like to have MyAppDevClient and MyApp installed at the same time – even better if I could have like MyAppDevClient, MyAppPreview, and then MyApp (live in stores).

This seems like a common use case but I’m failing to google properly. If someone could point me in the right direction I’d appreciate it.

here’s an example of how you can do this: GitHub - brentvatne/jonathanexample - refer to eas.json and app.config.js. we plan on writing some docs for this pattern soon!

  "build": {
    "release": {
      "android": {
        "buildType": "app-bundle"
      },
      "releaseChannel": "pro"
    },
    "preview": {
      "android": {
        "buildType": "apk"
      },
      "ios": {
        "simulator": true
      },
      "releaseChannel": "sim"
    },
    "development": {
      "developmentClient": true,
      "distribution": "internal"
      "ios": {
        "scheme": "myapp-dev",
        "buildConfiguration": "Debug"
      },
      "android": {
        "buildType": "apk"
      }
    },
    "prerelease": {
      "releaseChannel": "pre",
      "distribution": "internal"
      "ios": {
        "scheme": "myapp-dev-pro",
        "buildConfiguration": "Release"
      },
      "android": {
        "buildType": "apk"
      }
    }
  },
  "cli": {
    "version": ">= 0.35.0",
    "requireCommit": true
  }
}

my config, 4 profile. release (prod code, prod db), preview/development (dev code, dev db), prerelease (dev code, prod db). run expo with --dev-client and --scheme myapp-dev, for example

I solved that by modifying app.config.ts on the fly (using plugins).

Here is the part of my app.config.ts file:

export const withBridgeGo: ConfigPlugin = (config) => {
   const isBuildingClient: boolean = process.env.BRIDGE_GO === 'true';
   const isBuildingDevApp: boolean = process.env.BRIDGE_DEV === 'true';

   if (isBuildingClient || isBuildingDevApp) {
      let postfix: string = '';

      if (isBuildingDevApp) {
         postfix = 'Dev';
      } else if (isBuildingClient) {
         postfix = 'Go';
      }

      console.log(`=== BUILD BRIDGE ${postfix.toUpperCase()} CLIENT ===`);

      const copy = { ...config };
      let oldIcon: string = './assets/images/old_icon.png';
      if (isBuildingDevApp) {
         oldIcon = './assets/images/old_icon_dev.png';
      }

      copy.name = `Bridge ${postfix}`;
      copy.icon = oldIcon;
      copy.scheme = `${copy.scheme}-${postfix.toLowerCase()}`;

      if (copy.ios) {
         copy.ios.bundleIdentifier = `${copy.ios.bundleIdentifier}-${postfix.toLowerCase()}`;
         copy.ios.icon = oldIcon;
         copy.ios.googleServicesFile = `./GoogleService-Info-${postfix}.plist`;
      }

      if (copy.android) {
         copy.android.package = `${copy.android.package}_${postfix.toLowerCase()}`;
         copy.android.icon = oldIcon;
      }

      console.log('=== CONFIG AFTER MODIFICATION ===');
      console.log(copy);

      return copy;
   }

   return config;
};

export default ({ config }: ConfigContext): ExpoConfig => {
   const mock = process.env.EXPO_MOCK || false;
   const SENTRY_AUTH_TOKEN = process.env.SENTRY_AUTH_TOKEN || null;
   const SENTRY_DSN = process.env.SENTRY_DSN || null;
   const BEACON_CLIENT_ID = process.env.BEACON_CLIENT_ID || null;
   const BEACON_API_KEY = process.env.BEACON_API_KEY || null;
   const FF_EARN_2 = process.env.FF_EARN_2 || null;

   const extra = {
      mock,
      SENTRY_DSN,
      FF_EARN_2,
   };

   config.hooks?.postPublish?.forEach((pp: PublishHook) => {
      if (pp && pp.config && pp.file) {
         if (pp.file === 'sentry-expo/upload-sourcemaps') {
            pp.config.authToken = SENTRY_AUTH_TOKEN;
         } else if (pp.file === './notify') {
            pp.config.BEACON_CLIENT_ID = BEACON_CLIENT_ID;
            pp.config.BEACON_API_KEY = BEACON_API_KEY;
         }
      }
   });

   // @ts-ignore
   let result = withTapResearch({
      ...config,
      extra,
   });
   result = withBridgeGo(result);

   // @ts-ignore
   return result;
};

Also, I set env vars in my eas.json file:

"development": {
      "extends": "_node",
      "releaseChannel": "dev",
      "distribution": "internal",
      "env": {
        "BRIDGE_DEV": "true",
        "FF_EARN_2": "true"
      },
      "android": {
        "buildType": "apk"
      },
      "cache": {
        "key": "9d704401-53fd-4abc-bd69-dabcba6f6f7d"
      }
    },
    "development_client": {
      "extends": "_node",
      "developmentClient": true,
      "releaseChannel": "dev",
      "distribution": "internal",
      "env": {
        "BRIDGE_GO": "true",
        "FF_EARN_2": "true"
      },
      "cache": {
        "key": "6030d110-ca5b-4b07-9bf1-ab4b5e59077c"
      }
    },

Our Expo Go app analog I call as Bridge Go and a dev version of an app I call Bridge Dev. The key is modify a package for Android or bundleIdentifier for iOS. I even replace app icons to have a clean separation on my device.

All of that works like a charm for iOS and Android!

As I know due docs in case if is used dynamic config (app.config.js) you cannot use autoIncrement feature of eas.json, is it true or something changed?

This feature is not intended for use with dynamic configuration (app.config.js). EAS CLI will throw an error if you don’t use app.json.

this is still the case. the implementation for auto increment is quite simple and it just modifies values in app.json. without having the code in front of me, one thing you could possibly try is to create both app.json and app.config.js and controlling the versions in app.json. see the example here for how you can use both files at the same time Configuration with app.json / app.config.js - Expo Documentation

yes, nice! Due docs two config files (dynamic and static) can be used at the same time:

You can access and modify incoming config values by exporting a function that returns an object. This is useful if your project also has an app.json . By default, Expo CLI will read the app.json first and send the normalized results to the app.config.js .

This look amazing for purposes to have few versions of app at same time! Good!