Hot deploy updates to Ionic 3 apps with CodePush

Published: January 16, 2017  •  Updated: November 16, 2017  •  ionic3, cordova

In the previous post we updated an Ionic Cordova app on the fly with Ionic Deploy. Ionic Deploy is not the only service that provides support for this kind of updates. In this post we will look at CodePush. A free service developed by Microsoft and running on Microsoft's cloud Azure. CodePush does the same thing as Ionic Deploy. It pushes updates directly to Cordova/PhoneGap and React Native apps installed on a device.

Before we start, we need to install the CodePush command line interface. CodePush does not have a web based user interface. Everything is controlled from the CLI.

npm install -g code-push-cli

As usual prepend sudo when you are working on Linux or on macOS

Then you need to register an account on CodePush.

code-push register

This opens a browser window and asks you to authenticate with either a GitHub or with a Microsoft account. After the successful authentication the page presents a key that you have to copy and paste into the command line. The CLI stores this token in the file .code-push.config in your home directory. When you are already registered the site shows you an error message. In that case you need to login with

code-push login

You can determine if you are logged in with code-push whoami and code-push logout logs you out from the service and deletes the file .code-push.config.

If you worked on another machine and forgot to logout there, you can determine all the current sessions with session ls and then delete the one you no longer need.

code-push session ls
code-push session rm <machineName>

Initial version

Like we did in the previous post we create a simple Ionic app and deploy it on a real device or on the Android emulator. We start with the blank starter template and add the Android platform.

ionic start demo blank
cd demo
cordova platform add android

Then we need to install the CodePush Cordova plugin. This is the plugin that checks for new updates and when available downloads and installs them on the device.

cordova plugin add cordova-plugin-code-push@latest

Next we add a visual clue to our app, so we see if the updates were installed successfully. Open the file src\pages\home\home.html and add the following content.

<ion-header>
  <ion-navbar>
    <ion-title>
      Code Push
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-card>
    <ion-card-header>
      Version
    </ion-card-header>
    <ion-card-content>
      0.0.1
    </ion-card-content>
  </ion-card>
</ion-content>

The CodePush Cordova plugin does not automatically check for new updates, the app controls this from the code. The easiest way is by calling the codePush.sync() function. There are many different ways where to call this function.

For example an app can call it when the platform ready event is fired, although this event would only be fired when the app is started.

 constructor(private platform: Platform) {
    platform.ready().then(() => {
        codePush.sync();
    });
 }

If the app should check more frequently for updates you can add a resume listener

platform.resume.subscribe(() => codePush.sync());

This event will be fired every time the user resumes the app from the background.

Other options are a 'Check for Update' function in the app that the user has to manually start or a timer based approach

//check for new updates 5 seconds after the start and then every hour
const source = Observable.timer(5000, 1000*60*60);
const subscription = source.subscribe(() => codePush.sync());

This solution is a bit problematic, because sync() does automatically restart the app for mandatory updates. This could be a problem when the user is working with your app and entering some data.

For this demo I add a listener to the resume event.

export class MyApp {
  rootPage: any = HomePage;

  constructor(private platform: Platform,
              private statusBar: StatusBar,
              private splashScreen: SplashScreen) {
    platform.ready().then(() => {
      statusBar.styleDefault();
      splashScreen.hide();

      platform.resume.subscribe(() => 
          codePush.sync(null, {deploymentKey: 'b5s7XEMEPVsDhP54yK7_BHulXFg2V16RCXmIG '}));
    });
  }
}

https://github.com/ralscha/blog/blob/master/codepush/src/app/app.component.ts

Next we need to register the app with the CodePush service. The appName is an arbitrary name and does not need to correspond with a setting in the project.

code-push app add <appName>

When you create an app for Android and iOS, CodePush recommends to registers two appliations. Most users use this naming convention.

code-push app add appName-Android
code-push app add appName-iOS

The CLI provides management tasks to rename and delete applications.

code-push app rename <appName> <newAppName>
code-push app rm <appName>

For this demo I register the app with the name demo

code-push app add demo

The command returns the following output when successful.

Successfully added the "demo" app, along with the following default deployments:
┌────────────┬───────────────────────────────────────┐
│ Name       │ Deployment Key                        │
├────────────┼───────────────────────────────────────┤
│ Production │ QaEC_m5jK-4raXUfTsT9RGTjQIH9V16RCXmIG │
├────────────┼───────────────────────────────────────┤
│ Staging    │ b5s7XEMEPVsDhP54yK7_BHulXFg2V16RCXmIG │
└────────────┴───────────────────────────────────────┘

Similar to the channels in Ionic Deploy, CodePush supports multiple deployments per app. For every new app CodePush creates two deployments: Production and Staging. You can add more deployments with the command

code-push deployment add <appName> <deploymentName>

And you can delete and rename deployments

code-push deployment rm <appName> <deploymentName>
code-push deployment rename <appName> <deploymentName> <newDeploymentName>

Every deployment has a key associated with it. The application has to know this key to make an update check. You can either add this key to the config.xml file or add a parameter to the codePush.sync() function.

When you add the key to the config.xml file it has to follow this format.

<platform name="android">
    <preference name="CodePushDeploymentKey" value="YOUR-ANDROID-DEPLOYMENT-KEY" />
</platform>
<platform name="ios">
    <preference name="CodePushDeploymentKey" value="YOUR-IOS-DEPLOYMENT-KEY" />
</platform>

You don't need to add both platforms when the app only supports one.

The other approach is providing the key in the option object.

codePush.sync(null, {deploymentKey: 'b5s7XEMEPVsDhP54yK7_BHulXFg2V16RCXmIG'});

This allows a more dynamic approach where users can change the deployment channel in the app.

Another thing we need to check in the config.xml file is the access element. If the config contains this line then the app is able to communicate with the CodePush servers.

<access origin="*" />

When you restricted the access you have to add the CodePush servers.

<access origin="https://codepush.azurewebsites.net" />
<access origin="https://codepush.blob.core.windows.net" />
<access origin="https://codepushupdates.azureedge.net" />

When src/index.html contains a CSP header you have to add the CodePush server too.

<meta http-equiv="Content-Security-Policy" 
      content="default-src https://codepush.azurewebsites.net 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; 
               style-src 'self' 'unsafe-inline'; media-src *" />

And finally make sure that the cordova-plugin-whitelist is installed. In our case this plugin is already installed, because the Ionic starter templates do this automatically.

Our app is now finished and we can install it on a device

ionic cordova run android 

or in the emulator

ionic cordova emulate android

Update

Next we create a change in our app. We simply increase version number on our home page

    <ion-card-content>
      0.0.2
    </ion-card-content>

Now we have to build the app. The CodePush CLI takes everything that is in the www folder of the project and starts a cordova build. If you forget this step an old version will be published.

npm run build --prod

After the build we can release the update

code-push release-cordova demo android -m

By default this command releases the update to the Staging deployment. If you need to release an update to a specific deployment channel add the --deploymentName option.

code-push release-cordova demo android -m --deploymentName Production

The -m flag denotes this update as a mandatory update. By default, the codePush.sync() function downloads, installs and restarts the app for mandatory updates. For non essential updates it only downloads the update to the device and during the next restart (not resume) it will be installed.

To check if the update process is working in our demo app we need to trigger the resume event. When the app is in the foreground press the home button on the device and then open the app again. After a while you should see the updated home page with the updated content '0.0.2'.

Advanced sync() calls

If you need more insight into the update process you can provide two callback function as first and third argument to the codePush.sync() function.

codePush.sync(syncCallback?, syncOptions?, downloadProgress?);

The first callback is called when the sync task changes (from checking to downloading, from downloading to installing). The downloadProgress callback is called periodically when the plugin downloads the update package.

codePush.sync(syncStatus, null, downloadProgress);

function syncStatus(status) {
    switch (status) {
        case SyncStatus.DOWNLOADING_PACKAGE:
            break;
        case SyncStatus.INSTALLING_UPDATE:
            break;
    }
}

function downloadProgress(downloadProgress) {
    if (downloadProgress) {
        console.log("Downloading " + downloadProgress.receivedBytes + " of " 
              + downloadProgress.totalBytes);
    }
}

With the second argument an app can configure many aspects of the sync call. See the official documentation about the sync() function.

Note that you should not set the updateDialog option to true if your app is running on iOS. This option tells sync() to open a dialog when a new update is available.

codePush.sync(null, { updateDialog: { title: "An update is available!" } });

While Apple allows on the fly updates for Cordova apps it is against their policy for an app to display an update prompt. There is no such restriction on the Google Play store.

Promote

When you use the predefined deployment workflow you first release an update to the Staging deployment. Then you have testers with your app, that checks for updates on this deployment channel. When they give their okay and everything works you can promote the update from Staging to the Production deployment. You could do it manually by releasing an update to the Production deployment

code-push release-cordova demo android -m --deploymentName Production

But you have to make sure that you release exactly the same files you released to the Staging deployment. A better and quicker option is the promote command.

code-push promote <appName> <sourceDeploymentName> <destDeploymentName>

This copies the update package from Staging to Production.

Advanced workflows

codePush.sync() is a very convenient function that does everything (check, download, install and restart) in one function call. But when you need more control about the upgrade process you should look at the other functions that the plugin provides.

Function Description
checkForUpdate Makes a request to the CodePush server and check if an update is available
getCurrentPackage Retrieves the metadata about the currently installed update.
getPendingPackage Retrieves the metadata for an update (if one exists) that was downloaded and installed, but hasn't been applied yet via a restart.
notifyApplicationReady Notifies the CodePush runtime that an installed update is considered successful
restartApplication Immediately restarts the app and applies a pending update.

CodePush has many additional feature we did not cover in this post. For example there is an option to release an update only to a specified percent of users and it supports rollback of updates. See the CodePush documentation for a complete description: http://microsoft.github.io/code-push/docs/getting-started.html

You find the code for this example on GitHub.