Back

A 12-Factor Approach to Environment-Specific Builds in React Native

Chris Ball
Chris Ball
August 7, 2018
A 12-Factor Approach to Environment-Specific Builds in React Native

Having separate builds for dev, beta, and production environments is critical for most apps.

These environment specific builds give us a way to:

  • Change the values of variables at build time
  • Change app/bundle ids to allow the installation of any environment variant on the same device at the same time
  • Change the icon for each build variant
  • Change the display name of the app

Let’s look at how to do that in React Native!

There are two main ways to configure environment-specific builds in your app, and neither of them are the following approach:

// development const API = 'http://localhost:4000'; // staging // const API = 'https://my-staging-server.com'; // production // const API = 'https://my-prod-server.com';

Cat hugging another cat with Don't do it Carl! You're on your 9th life quote

The traditional (manual) approach

The first way to set up environment-specific builds involves using schemes and build configurations in Xcode, and buildTypes or productFlavors for Android. You then have to set up user-defined variables in Xcode and create separate .properties files for Android. But guess what? You now have to manage the values in both places.

It would be nice to have a single place to do this, and ideally a way to do so without touching native code.

The 12-factor approach

At Echobind, we follow the 12-factor approach for all of our web and mobile apps. Using this approach, you create a single version of your app configured differently via environment variables. We see multiple wins by taking this approach in React Native:

  • It’s harder to accidentally build and submit a release of the wrong type and for example, submit a beta build to the App Store.
  • There aren’t multiple configurations to manage differently (one for iOS and one for Android)
  • If you .gitignore your .env files, developers don’t have access to staging or production configurations by default, which increases security.

In React Native, we can gain access to ENV variables by using react-native-config. babel-transform-inline-environment-variables is an alternative option, but it doesn’t currently work in the App Center environment. We use App Center on a lot of client projects (expect an App Center specific post from us soon!), so we stick with react-native-config. It requires a bit more setup, but you also get the added benefit of having access to environment variables in native code, and configuration files if you need them (such as Info.plist on iOS and strings.xml on Android).

react-native-config expects an .env file which defines all the environment variables used in your app. We strongly believe that .env files should not be checked into source control, so we needed a way to provide one to our CI server.

.env on CI

Most CI’s support build scripts to run custom commands at various points in the build pipeline. We can leverage these scripts to create a .env file for us. Here’s what an example appcenter-pre-build.sh script looks like, which creates an .env dynamically:

#!/usr/bin/env bash # Creates an .env from ENV variables for use with react-native-config ENV_WHITELIST=${ENV_WHITELIST:-"^RN"} printf "Creating an .env file with the following whitelist:\n" printf "%s\n\n" $ENV_WHITELIST set | egrep -e $ENV_WHITELIST | egrep -v "^_" | egrep -v "WHITELIST" > .env printf "\n.env created with contents:\n" cat .env

This script takes a configurable whitelist of ENV variables (defaulting to anything with an RN prefix), and uses set and -->egrep--> to filter available ENV variables down to only the variables that match that whitelist regex. The output is saved to an .env file which react-native-config reads before building our app.

Changing variables at build time

With react-native-config setup, changing your API server or any other variable is now just a matter of:

  • Using Config.ENV_NAME in place of a hardcoded value in your app code.
  • Updating your local .env file to have a sensible default.
  • Adding an ENV variable to your CI server with the beta or production value.
  • Informing your team members. We like to keep an .env.example in the project root that contains all the variables (without real values) that need to be set.

Change the bundle / app id for each environment

Each app has an id assigned to it (typically com.yourcompany.AppName). This is called a bundle identifier on iOS and an app id on Android. If the beta build of your app and release build of your app share an id, you can’t install both at the same time on the same device. To get around this, we dynamically assign a different id at build time.

We need a way to add a suffix to the id for each release type:

com.yourcompany.AppName
com.yourcompany.AppName.alpha
com.yourcompany.AppName.beta

Tools like PlistBuddy and Gradle scripts can help with this, but we’re trying to do things cross-platform in a unified way. Enter fastlane. To setup fastlane, follow the Getting Started section in the guides.

To start, we’ll create a basic lane that will be used to update the bundle / app id. Create a fastlane folder with the following 2 files in the root of your project:

Fastlane folder with two files nested under it

`fastlane init` will complain about finding an ios project in a subfolder, so just create these manually.

Update Appfile with the app_identifier and package_name that your app uses. This will avoid prompts from fastlane to request these values.

# iOS Settings app_identifier("com.yourcompany.YourApp") # Android Settings package_name("com.yourcompany.YourApp")

Next, add the magic. fastlane comes bundled with an ios plugin called update_info_plist that we will use to update the app_id. On Android, we’ll leverage a built-in concept called applicationIdSuffix and install a plugin that does the same thing. Add the following a line to android/app/build.gradle, and set it to an empty string by default.

CSS Code

After setting up react-native-config, this is the only “native” change you’ll have to make

Next, install the set_value_in_build plugin:

Terminal with install data

As of August, 1, 2018, this plugin doesn’t properly update values that contain empty strings (I have an open PR that fixes it). In the meantime, if you want to follow the same process here, you’ll need to point to a fork.

Update the fastlane/Pluginfile that was created when you installed the plugin to point to the fork:

# Autogenerated by fastlane # # Ensure this file is checked in to source control! gem 'fastlane-plugin-android_versioning', github: "cball/fastlane-plugin-android_versioning", ref: "support-blank-id-suffix"

Run bundle install to install the updated gem.

To use these plugins, update your Fastfile to the following:

# this should be the folder name under `ios` for your project project_name = 'MyProject' # NOTE: This is meant to be run on CI where it changes everything before building the app. # Usage: # `RN_RELEASE_TYPE=beta fastlane prep_release_type` (on CI these ENV variables should be set via the UI) desc "Updates the app identifier, display name and icon for alpha, beta, and production releases" lane :prep_release_type do # alpha, beta, production type = ENV['RN_RELEASE_TYPE'] || 'production' next if type == 'production' suffix = ENV['RN_BUNDLE_SUFFIX'] || type # assumes identifier is defined in Appfile app_id = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) new_app_id = "#{app_id}.#{suffix}" # update the app identifier and bundle UI.message "\n\nSetting app identifier to: #{new_app_id}" # update ios indentifier update_info_plist( plist_path: "#{project_name}/Info.plist", xcodeproj: "./ios/#{project_name}.xcodeproj", app_identifier: new_app_id ) # update android suffix set_value_in_build( app_project_dir: "./android/app", key: "applicationIdSuffix", value: ".#{suffix}" ) end

In this lane, we provide a configurable release type. We typically use alpha, beta, and production, but additional types can be added by changing the environment variable.

We then set a customizable suffix, which defaults to the release type.

Time to test it out! Run the following command from your project root. The use of bundle exec is important to ensure we execute the command in the context of our project Gemfile.

RN_RELEASE_TYPE=beta bundle exec fastlane prep_release_type

You should see the following updates:

GitHub code changes

ios changes

GitHub code changes

android changes

Remember, this is designed to be run on CI and discarded after build, so it’s fine that these values get overwritten.

Change the Icon for Each Environment

Now that we’re able to install the different builds side-by-side we need to make the app icon visually distinctive so we know which one we’re opening.

To do this, we’ll make use of a great plugin for fastlane called badge.

If you don’t already have Imagemagick installed, do that first (brew install imagemagick on a mac).

Next install the badge plugin:

fastlane add_plugin badge

Insert an add_badge command to the existing lane for each platform. If you have a typical icon setup, the following should just work for you. Otherwise, you may have to adjust the glob line for each platform.

lane :prep_release_type do # ... # Add badge to app icon # want to add the app version to the icon? try this: # current_version = get_version_number(xcodeproj: "./ios/#{project_name}.xcodeproj", target: project_name) # add_badge( # shield: "Version-#{current_version}-blue" # shield_scale: 0.75 # ) # # See all available options at: https://github.com/HazAT/badge UI.message "\n\nUpdating app icon with #{type} badge" # ios badge add_badge( glob: "/ios/**/*.appiconset/*.{png,PNG}", # note no dot in path alpha: type == 'alpha', grayscale: type == 'alpha' ) # android badge add_badge( glob: "/android/app/src/main/res/**/*.{png,PNG}", # note no dot in path alpha: type == 'alpha', grayscale: type == 'alpha' ) end

Now, we can re-run our previous command to produce some slick looking badges!

RN_RELEASE_TYPE=beta bundle exec fastlane prep_release_type
RN_RELEASE_TYPE=alpha bundle exec fastlane prep_release_type

Left image is fastlane logo in color and right image is fastlane logo with Beta in bottom right

Example of our beta icon

Left image is black and white fastlane logo and right image is fastlane logo with Beta in bottom right

Example of our alpha icon

Note: Like the app id changes, it will modify the icon in place. If you’re testing this out locally, just discard the changes.

Changing the display name

For iOS, we just have a minor update, but to handle Android we need to install another plugin to update our strings.xml file:

fastlane add_plugin update_android_strings

Now we can update our lane to change the display name on both platforms:

lane :prep_release_type do # ... display_name = ENV['RN_DISPLAY_NAME'] || type.capitalize UI.message "\n\nSetting Display Name to: #{display_name}" # update ios indentifier and display name update_info_plist( plist_path: "#{project_name}/Info.plist", xcodeproj: "./ios/#{project_name}.xcodeproj", display_name: display_name, # <------------------ add this line to our existing update_info_plist call app_identifier: new_app_id ) # update android display name update_android_strings( block: lambda { |strings| strings['app_name'] = display_name } ) # ... end

Environment-specific builds for the win

Welcome to a scalable, secure way to set up variables, icons, display names, and bundle suffixes for different environments your app needs. You should be able to set up any CI to run your Fastlane lane, overriding the defaults via ENV variables as needed.

Here’s our final Fastfile for reference:

# this should be the folder name under `ios` for your project project_name = 'MyProject' # NOTE: This is meant to be run on CI where it changes everything before building the app. # Usage: # `RN_RELEASE_TYPE=beta fastlane prep_release_type` (on CI these ENV variables should be set via the UI) # Available release types: alpha, beta, production (default) # # If you're trying this script out locally, make sure you have ImageMagick installed, and discard the changes via git when you're done. desc "Updates the app identifier, display name and icon for alpha, beta, and production releases" lane :prep_release_type do # alpha, beta, production type = ENV['RN_RELEASE_TYPE'] || 'production' next if type == 'production' suffix = ENV['RN_BUNDLE_SUFFIX'] || type # assumes identifier is defined in Appfile app_id = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) new_app_id = "#{app_id}.#{suffix}" display_name = ENV['RN_DISPLAY_NAME'] || type.capitalize UI.message "\n\nSetting app identifier to: #{new_app_id}" UI.message "\n\nSetting Display Name to: #{display_name}" # update ios indentifier and display name update_info_plist( plist_path: "#{project_name}/Info.plist", xcodeproj: "./ios/#{project_name}.xcodeproj", display_name: display_name, app_identifier: new_app_id ) # update android display name update_android_strings( block: lambda { |strings| strings['app_name'] = display_name } ) # update android suffix set_value_in_build( app_project_dir: "./android/app", key: "applicationIdSuffix", value: ".#{suffix}" ) # Add badge to app icon # want to add the app version to the icon? try this: # current_version = get_version_number(xcodeproj: "./ios/#{project_name}.xcodeproj", target: project_name) # add_badge( # shield: "Version-#{current_version}-blue" # shield_scale: 0.75 # ) # # See all available options at: https://github.com/HazAT/badge UI.message "\n\nUpdating app icon with #{type} badge" # ios badge add_badge( # use dark if your icon is light in color # dark: true, glob: "/ios/**/*.appiconset/*.{png,PNG}", # note no dot in path alpha: type == 'alpha', grayscale: type == 'alpha' ) # android badge add_badge( # use dark if your icon is light in color # dark: true, glob: "/android/app/src/main/res/**/*.{png,PNG}", # note no dot in path alpha: type == 'alpha', grayscale: type == 'alpha' ) end

Need help with your React Native app? Say hello! We’d love to chat.

Share this post
Related Posts:

Interested in working with us?

Give us some details about your project, and our team will be in touch within a day or two.