HotCodePush

Switch channels by link

Register a URL scheme, handle the link in React Native, Expo, Capacitor or Cordova and call setChannel with the channel id it carries.

A deep link moves a device to a channel in one tap. The link carries the channel's id, and the app's handler reads the id out of the path and calls setChannel({ id }). The next sync picks up that channel's newest eligible release.

HotCodePush renders the link as a QR code for every channel and ships no deep-link code of its own. So the URL scheme and the handler are the app's. Test with preview channels owns the channel per pull request and the pipeline that creates it.

Prerequisites

  • Quickstart has run, so the SDK is installed and a first release is live.
  • Channels explains how a build, setChannel or a link picks the channel a device follows.

If the template is set, skip to step 2. The template is the deep link behind every channel's QR code, set once per app in the console. On the app's Settings page, the Channel link template island takes the Link, for example myapp://preview/{channelId}.

It is a URI with a scheme that carries {channelId} exactly once, and no CLI flag sets it. From then on GET https://api.hotcodepush.com/v1/apps/{appId}/channels/{channelId}/qr-code renders the QR code without a token, SVG by default and PNG with ?format=png.

2. Register the URL scheme

If the app already opens on myapp://, skip to step 3. The snippets use myapp. Pick a scheme of your own.

The handler of step 3 reads the URL through the @capacitor/app plugin:

npm install @capacitor/app
npx cap sync

On iOS, register the scheme in Info.plist:

ios/App/App/Info.plist
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.example.app</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>

On Android, the main activity's intent filter names the scheme through the string custom_url_scheme. Set the string to the scheme:

android/app/src/main/res/values/strings.xml
<string name="custom_url_scheme">myapp</string>

Then add the filter inside the <activity> element where the manifest lacks it:

android/app/src/main/AndroidManifest.xml
<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="@string/custom_url_scheme" />
</intent-filter>

The handler does the same on every framework, and only the API that delivers the URL differs. It takes the URL the app was opened with, at launch and while running, and reads the channel id out of the path. If the id is a UUID, it calls setChannel({ id }) and then sync().

The snippets assume the template myapp://preview/{channelId}, the id being the last path segment.

src/channel-link.ts
import { App } from '@capacitor/app';
import { HotCodePush } from '@hotcodepush/capacitor-live-updates';

const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

export async function listenForChannelLinks(): Promise<void> {
  const launch = await App.getLaunchUrl();
  if (launch?.url) {
    await switchChannelFromLink(launch.url);
  }
  await App.addListener('appUrlOpen', ({ url }) => {
    void switchChannelFromLink(url);
  });
}

async function switchChannelFromLink(url: string): Promise<void> {
  const channelId = url.split('/').pop() ?? '';
  if (!UUID_PATTERN.test(channelId)) {
    return;
  }
  await HotCodePush.setChannel({ id: channelId });
  await HotCodePush.sync();
}

Call listenForChannelLinks() once at app start, where the app sets up its other listeners. The Cordova file runs at page load and waits for deviceready itself.

The setChannel call stores the choice across restarts. The sync() after it downloads the channel's newest eligible release and applies it at the next start by default, or as the apply strategy says.

4. Return to the build's channel

A preview channel ends: it expires, or the pipeline deletes it when the pull request closes. Its index is gone, and from then on every check on a device that still follows it fails with channel_unknown. The device keeps the bundle it runs and its channel. The SDK never changes the channel on its own, so returning is the app's call.

The handler below does it: when a check fails with channel_unknown and the channel was set at runtime, it clears the choice. The source guard is what keeps it honest, since the app only undoes what the app did. A build whose own channel is gone is left alone, because clearing changes nothing there.

src/channel-link.ts
import { HotCodePush } from "@hotcodepush/capacitor-live-updates";

export async function returnWhenChannelIsGone(): Promise<void> {
  await HotCodePush.addListener('updateFailed', async ({ reason }) => {
    if (reason !== 'channel_unknown') {
      return;
    }
    const { source } = await HotCodePush.getChannel();
    if (source === 'runtime') {
      await HotCodePush.setChannel(null);
    }
  });
}

Call it next to listenForChannelLinks(). The device is back on the build's channel at its next check.

The handler also fires on a check that fails for a moment, for example while the channel's index cannot be served. On a tester's phone that costs one rescan. Leave the handler out for a tenant channel, which a device must keep: a tenant app logs the failure instead, and the device stays where the backend put it.

5. Test it

Open the channel in the console, click Install and scan the QR code with the phone that carries the build. Or send the link to the phone and open it there. The app opens, the handler runs, and getChannel() now answers the channel's id with source: 'runtime'.

Passing null to setChannel returns the device to the build's channel, and so does Return to configured channel on the SDK's debug screen. If nothing happens, see Troubleshooting.

The same link serves a tenant: the backend hands the app the tenant's channel id at sign-in. Set up environments and tenants shows the setup.

Next steps

Last updated on

On this page