Nishant Neeraj
There is No Spoon

There is No Spoon

Mastering Over-the-Air Updates in React Native with CodePush: Part 2

LIGHT SPEED UPDATES WITH GOODIES!

Nishant Neeraj's photo
Nishant Neeraj
·Aug 5, 2020·

10 min read

Mastering Over-the-Air Updates in React Native with CodePush: Part 2

Subscribe to my newsletter and never miss my upcoming articles

This post was originally published on Medium by me on Aug 5, 2020

This is the second and the final part of the two part series. Read the first part here.

As happy and dandy as we feel after setting up a pipeline that makes majority of app release bypass the app stores, but the happiness does not last long when you want to do a pre-release testing (Alpha Track in app store) or a beta testing with beta participants, or A/B Testing over 20% of all the users. Only if CodePush could do these for us. Well, you are in luck, CodePush can do all this and more. Let’s explore?

An App with Multiple Deployment Keys

Let’s try and get our app to do the following:

  1. Do not pull the updates automatically
  2. Pull the updates when the programmer or the user wants
  3. When the updates are pulled programmatically, do not apply the updates immediately (Because that may stun the user. Imagine you are filling up a long from, and just before the submit button was clicked, the app reboots. Not nice.)
  4. When user switches the deployment key (as in, they switch from Stable version to Beta version or vice versa), load the changes immediately
  5. Provide an “Open Sesame” to the users to switch between beta and stable versions.

MANUAL UPDATES

Remember the AppWrappedInCodePush.tsx file from Part 1? It looked laughingly small. We are going to make it the center of our deployment logic. Here is the full version of it:

☝️ AppWrappedInCodePush.tsx

☝️ constants.ts

Let’s read it through:

#35: We are now passing a parameter that tells the CodePush client to not pull the code automatically at the start up.

As soon as the main application is mounted, the lines #14–28 does a couple of things:

  1. Checks if AsyncStore has a value for the key IS_BETA_USER
  2. If that key is set to true, sync the codebase with beta stream (because we are using the Staging Key)
  3. Otherwise, sync the codebase with the stable stream as we pass the Production Key in this case
  4. Noteworthy, we are setting the installMode to codePush.InstallMode.ON_NEXT_RESTART. Because, from the point of view of the user, this update is happening automatically, and in the background. If you install the code immediately, causing a reboot of the app, the user may not like it. So, we download and wait for the next time the user opens the app to show them the shiny new app.

As of now, we have not set IS_BETA_USER to anything, so the behavior stays unchanged compared to what we had at the end of Part 1 of this article. But, we can see if somehow we set the IS_BETA_USER value to true in our AsyncStorage, we will start seeing the beta version of the app.

SWITCHING BETWEEN STABLE AND BETA VERSIONS

I like Easter Eggs in software. What we want to do here is to create a component that looks like plain text information of the software’s version. When a user taps it 8 times, we show them the option to switch between stable and beta versions with a fair warning.

Here is that component’s code look like:

☝️ VersionInfo.tsx

Line number #58 — #78 is the core of it. The rest is just the display logic.

The toggleBetaUser does the following:

  1. Sets the IS_BETA_USER value as true or false and stores it in AsyncStore for persistence
  2. Calls CodePush’s sync method with appropriate key with installMode as IMMEDIATE. This will cause the code to be downloaded from an appropriate release stream and apply it immediately after the download. This will cause the app to reboot and show the downloaded version.

Now that we have a mechanism to toggle and save the user preference to AsyncStore, our code in the first part of this article would work. If use has set beta mode on, it will sync with the Staging stream, otherwise CodePush will sync with the Production stream.

From left to right, top to bottom: 1. Stable version V5, 2. After the user tapped the version text 8 times, 3. The user opted-in for beta and assets are downloading, 4. User is now on beta version

☝️ From left to right, top to bottom: 1. Stable version V5, 2. After the user tapped the version text 8 times, 3. The user opted-in for beta and assets are downloading, 4. User is now on beta version


FUN THINGS TO DO WHILE SYNCING

MetaData: codePush.metadata() returns null or a LocalPackage object. Among other things, it has a couple of cool attributes. LocalPackage.appVersion gives you the version of the binary installed on the device. You can see 0.0.4 in the screenshot above. LocalPackage.label gives the CodePush’s resource version installed. It is indicated by v5 in production deployment, and v3 in staging deployment. You can also ask deploymentKey from LocalPackage.

Check for Updates: codePush.checkForUpdate() — This method returns a Promise<null|RemotePackage>. If null, there is nothing to do; all up to date. If it returns a package object, then you can either show to the user that a new update is available or pull the latest.

Synchronize at Will: codePush.sync() — This method kicks off synchronization. This packs a couple goodies. Here is the method signature:

/* 1.*/ codePush.sync(  
/* 2.*/ options: Object,  
/* 3.*/ syncStatusChangeCallback: (syncStatus: Number) => void,  
/* 4.*/ downloadProgressCallback: (progress: DownloadProgress) => void,  
/* 5.*/ handleBinaryVersionMismatchCallback: (update: RemotePackage) => void
/* 6.*/ ): Promise<Number>;

options is a JSON object. The most notable attributes are deploymentKey and installMode. The former is the deployment you want the app to sync with, and the latter is how you want the install to happen: immediately, after restart, when the app goes to background or any other way. There are other attributes that can be found in the documentation.

syncStatusChangeCallback is a function with its signature as (syncStatus: Number) => void, where syncStatus can be any of the values as shown in the enum below. This is what I am using in my code to display the spinner until the syncStatus reaches the UPDATE_INSTALLED state

downloadProgressCallback is a function with its signature as (progress: DownloadProgress) => void. You can get really fancy with this. You can use DownloadProgress.totalBytes and DownloadProgress.receivedBytes to show progress bar animation or something.

handleBinaryVersionMismatchCallback can be used to handle situations where you are trying to sync with a deployment whose binary version has diverged from the one on the local machine. This is your opportunity to remind the user to download the latest version from the app store.


ENFORCE CERTAIN USERS TO HAVE CERTAIN DEPLOYMENT

I hope you do not discriminate against your users on the basis of gender, race, region, belief or anything that they do not have their control over. That’s just not the nicest thing to do. But, it may make sense in certain cases to differentiate the users. What if you want to enable the beta version for all the users whose UserProfile object has isBetaUser = true in the database. Or, maybe, you wanted to roll out the beta version first in certain countries and based on their feedback, you want to decide whether to promote beta version globally.

In all such cases, wherever you fetching user profile just set AsyncStore.setItem(‘IS_BETA_USER’, `${userProfile.isBetaUser}`)

Ta-da!

Why stop here?

The binary is already embedded with the production key. So, everyone will get the production version to start with. If nothing is altered, they will continue to have production updates. Why can’t we, instead of having a binary value — IS_BETA_USER and hardcoded Deployment Key, dynamically set Deployment Key in AsyncStore and use that key to sync?

Different UIs based on your thumb size?

☝️ Different UIs based on your thumb size?

That means, we can set different Deployment Keys for different users and make releases specific to them. For example, if your sync logic looks like this:

// undefined userProfile.deploymentKey would use   
// the key stored in the binary  
codePush.sync({  
 deploymentKey: userProfile.deploymentKey,  
 installMode: codePush.InstallMode.ON_NEXT_RESTART,  
});

A hypothetical ticket to make a completely new release only to a specific group of users and its workflow would look like this:

TICKET #666: Make a deployment specific to Indian and Australian users.
DevOps: Well, since deployment keys are just a name and an identifier, let’s create one for this ticket

# Create a new deployment key  
# appcenter codepush deployment add <KEY_NAME> --app <USER>/<APP_NAME>
appcenter codepush deployment add colonial_cousins --app nishant/blog-app

Deployment colonial_cousins has been created for nishant/blog-app with key nkaKawu1CtuHuzzahGIJWq_LOTVcAHEcyLEWt

DBA: Let’s set users in India and Australia with deployment_key as nkaKawu1CtuHuzzahGIJWq_LOTVcAHEcyLEWt, others may be null or whatever value they are set to.

UPDATE user_profiles   
SET deployment_key = ‘nkaKawu1CtuHuzzahGIJWq_LOTVcAHEcyLEWt’   
WHERE country IN (‘AU’, ‘IN’)

Release Engineer: Well, let’s push this release to Indians and Aussies, and close the ticket.

# Release to specific deployment  
# appcenter codepush release-react -a <USER>/<APP_NAME> -d <KEY_NAME>

appcenter codepush release-react -a nishant/blog-app -d colonial_cousins

There we go. We did not touch client code, we did not ask the user to do something special, and still, we have managed to distribute a targeted software update. Isn’t it smooth?


A/B TESTING

Dark Mode vs Light Mode?
Tabs vs drawer?
Automated grouping or ungrouped messages?
How much time spent on the app on an average?

Sometimes, you want to be cautious, and release an upgrade to a percentage of users first and then, based on analytical data, you want to decide whether to go ahead with the upgrade or rollback. CodePush makes it as simple as specifying a parameter in the release command. You can specify what percentage of the users get this release. Here is an example

# appcenter codepush release-react -a <USER>/<APP_NAME> -d <KEY_NAME> -r <PERCENTAGE_ROLLOUT>

# The following will roll out the blog app to 20% of the users who are having Production deployment

appcenter codepush release-react -a nishant/blog-app -d Production -r 20%

PROMOTING STAGING RELEASE TO PRODUCTION

A common scenario is where once you have tested the app in staging, you want everyone else to have that version. It is a good practice, because it gives you confidence that the tested bundle is going to the production users, not a single bit different.

You can do it by running the promote command

#appcenter codepush promote -s <SOURCE_DEPLOYMENT_NAME> -d <DESTINATION_DEPLOYMENT_NAME> -a <USER>/<APP_NAME>

appcenter codepush promote -s Staging -d Production -a nishant/blog-app

Want to be super cautious and promote only to partial users? Just use the -r argument along with the other options above.

As always, Deployments are just names and identifiers, you can promote any deployment to any deployment.


BINARY TARGETED RELEASE

Imagine you have two binaries in app stores, one versioned 42.69.8 and another 43.0.0. The older version has some bugs which are fixed in the newer version, but the code in the newer version is incompatible to the code in the older version (maybe because you changed REST response, or added a new node module that’s not there in the older version).

You want to make a targeted release to the 42.69.8 binary release version.

# Release to specific deployment

# appcenter codepush release-react -a <USER>/<APP_NAME> -d <KEY_NAME> -t <VERSION>

appcenter codepush release-react -a nishant/blog-app -d Production -t 42.69.8

FANCIER THINGS

It should be clear to you by now that a deployment is just a name and an identifier associated with it. So, can we use it in our JavaScript as a name and an identifier attached to it? Of course!

You can use the identifier as the environment name. You can have things like this:

// I am assuming you have method somewhere to get currently  
// used deployment key and a constants object that holds  
// the deployment keys

const isStaging =  
  getDeploymentKey() === constants.STAGING_DEPLOYMENT_KEY;

export const API_ROOT_URL = isStaging  
  ? 'https://test.tehbesttodo.dev/api'
  : 'https://production.tehbesttodo.app/api';

I think I will close at this. There are many other helpful commands and tricks to use with CodePush, you just need to peek out of the box a little and read the documentation: https://docs.microsoft.com/en-us/appcenter/distribution/codepush/. Definitely, the latter.

If you liked the article please do not hesitate to slap the clap button for 50 times. Yes, you can. It is not tiring at all! And share it may be?

 
Share this