ReactNativeアプリに強制アップデート機能を導入する方法

この記事は [クラウドワークス Advent Calendar 2018](https://qiita.com/advent-calendar/2018/crowdworks) の12日目の記事です。

こんにちは。エンジニアの [kinakobo](https://github.com/kinakobo) です。

最近アプリの開発をReactNativeで行ったのですが、強制的にアプリをアップデートする機能をどう実装するかちょっと悩みました。
私のようにアプリ開発はReactNativeが初めてという方は同じような悩みを持つかもしれないので、今回行った方法を紹介しようと思います。

## 強制アップデート機能とは？
端末にインストールされているアプリが要求バージョンを満たさないときにアラートなどでアップデートを促す機能です。

<img width="375" alt="Image.png" src="https://qiita-image-store.s3.amazonaws.com/0/105475/f1b8a24f-0c66-16cb-b34d-5fef65b88cdb.png">

「後で通知」ボタンがなかったり、いくつかパターンはあるかと思います。

この機能、アプリを使っているとたまに見かけると思うのですが、正式名称がわからず検索ワードに困りました。
「強制アップデート機能」でいくつか関連記事が見つかったので、この記事では「強制アップデート機能」と呼ぶことにします。

## アプリの最新バージョンをどう取得するか
強制アップデート機能を実装するためには、アプリの最新バージョンを何らかの方法で取得する必要があります。

まずはReactNativeで定番の方法があるか調べてみましたが、残念ながら見つかりませんでした。
なのでiOS、Androidアプリでどのような方法を取っているかを調べてみました。

すると、iOSでは[iTunesSearchAPI](https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/)というものが提供されており、これを使用して配信されているアプリの最新バージョンを取得できることがわかりました。
以下のURLを叩くとアプリの最新バージョンを含むレスポンスを取得できます。

```
https://itunes.apple.com/lookup?id=${appId}&country=JP // appIDはAppStoreのURLから確認できるアプリ固有の数値
```

ということは、Androidでも似たようなAPIが提供されているかもしれないと思いましたが、そのようなものは見つかりませんでした。
Playストアのページをスクレイピングして取得することもできますが、PlayストアのHTMLに変更があった場合に対応が必要になることを考えると面倒です。

ではAndroidアプリではどのような方法で最新バージョンを取得しているのか調べてみると、以下の方法が行われていました。

- サーバーに最新バージョンを記述したjsonファイルなどを置く
- 最新バージョンを返すAPIを作る

どちらもサーバーに仕組みを用意するという点では変わりません。
jsonを置く方が実装は簡単ではあります。
APIを作る方はバージョンをハードコードしてもいいですし、環境変数などで切り替えられるようにするとデプロイ不要でバージョンを切り替えられそうです。

この方法ならiOS、Android関係なく同じ仕組みで最新バージョンを取得することができるので良さそうです。

サーバーがない場合は[Lambda](https://aws.amazon.com/jp/lambda/)や[CloudFunctions](https://cloud.google.com/functions/docs/?hl=ja)などでサーバーレスなAPIにしても良いと思います。
さらにお金をかけたくない場合は[FirebaseRemoteConfig](https://firebase.google.com/docs/remote-config/?hl=ja)というサービスを利用する方法も良いかと思います。これについては後ほど説明します。

また、別の方法として[CodePush](https://microsoft.github.io/code-push/)を導入していれば古いバージョン向けに強制アップデートのアラートを表示するJSコードを配信することもできますが、リリース作業が複雑になりそうであまり現実的ではないと思います。
そんなことをせずともCodePushで最新バージョンのコードを配布してしまえば良いかと一瞬思いましたが、CodePushではネイティブコード部分を更新できないので困るケースが出てきます。

## アプリ側の実装
最新バージョン取得の方法がわかったので、次にアプリ側の実装を簡単に紹介します。
アプリ側で行うこととしては以下になります。

- 最新のアプリと現在利用しているアプリのバージョンを取得して比較する
- 更新を促すアラートを表示する
- ストアのURLを開く

利用しているアプリのバージョンを取得する為に[react-native-device-info](https://github.com/rebeccahughes/react-native-device-info)というライブラリを使用しています。
使用したことはありませんが、より軽量なものだと[react-native-version-check](https://github.com/kimxogus/react-native-version-check)というライブラリがあります。

```typescript

import { Alert, Linking, Platform } from "react-native";
import DeviceInfo from "react-native-device-info";

// アプリの最新バージョンを取得する実装
const latestAppVersion = XXXX
// 現在利用しているアプリのバージョンを取得する
const appVersion = DeviceInfo.getVersion();

// 新しいバージョンのアプリがストアに配布されている場合は更新を促す
if (appVersion < latestAppVersion) {
  showUpdateAlert();
}

// アラートの表示
function showUpdateAlert() {
  Alert.alert("更新情報", "新しいバージョンが利用可能です。最新版にアップデートしてご利用ください。", [
    { text: "後で通知", style: "cancel" },
    { text: "アップデート", onPress: () => openStoreUrl() }
  ]);
};

// ストアのURLを開く
function openStoreUrl() {
  // iOSとAndroidでストアのURLが違うので分岐する
  if (Platform.OS === "ios") {
    const appId = XXXXXX; // AppStoreのURLから確認できるアプリ固有の数値
    const itunesURLScheme = `itms-apps://itunes.apple.com/jp/app/id${appId}?mt=8`;
    const itunesURL = `https://itunes.apple.com/jp/app/id${appId}?mt=8`;

    Linking.canOpenURL(itunesURLScheme).then(supported => {
      // AppStoreアプリが開ける場合はAppStoreアプリで開く。開けない場合はブラウザで開く。
      if (supported) {
        Linking.openURL(itunesURLScheme);
      } else {
        Linking.openURL(itunesURL);
      }
    });
  } else {
    const appId = "com.example"; // PlayストアのURLから確認できるid=?の部分
    const playStoreURLScheme = `market://details?id=${appId}`;
    const playStoreURL = `https://play.google.com/store/apps/details?id=${appId}`;

    Linking.canOpenURL(playStoreURLScheme).then(supported => {
      // Playストアアプリが開ける場合はPlayストアアプリで開く。開けない場合はブラウザで開く。
      if (supported) {
        Linking.openURL(playStoreURLScheme);
      } else {
        Linking.openURL(playStoreURL);
      }
    });
  }
};
```

上記のコードをアプリ起動時に実行する場合は`componentDidMount()`などに書きます。
アプリがバックグラウンドからフォアグラウンドに復帰した際に実行したければ、[AppState](https://facebook.github.io/react-native/docs/appstate)を利用して書くと良さそうです。

## RemoteConfigの導入方法
先ほど名前を出したRemoteConfigを使用する方法は一般的ではないと思うので、導入方法を簡単に紹介します。
RemoteConfigとは、アプリ向けに任意の値を返すAPIを提供できるサービスです。

必要なライブラリは[react-native-firebase](https://github.com/invertase/react-native-firebase)です。
このライブラリはReactNativeでFirebaseのサービスを使う際に利用することが多いので、FirebaseAnalyticsなどを利用している場合はすでに導入してあるかもしれません。

導入手順はライブラリの公式ドキュメントに沿って行えばOKです。
1. react-native-firebaseのセットアップ https://rnfirebase.io/docs/master/installation/initial-setup
2. RemoteConfigモジュールのセットアップ https://rnfirebase.io/docs/master/config/ios
3. アプリ側のコードサンプル https://rnfirebase.io/docs/master/config/example

あとはこのような管理画面で任意のキーと値を設定することで、アプリ側からこの値を取得することができます。

![Remote_Config_–_Firebase_console.png](https://qiita-image-store.s3.amazonaws.com/0/105475/a6e05c10-49e7-ace6-99a0-8395a35921d0.png)

注意点として、RemoteConfigで取得した値はデフォルトでは12時間キャッシュされ、その間再取得されません。
[fetch関数](https://rnfirebase.io/docs/master/config/reference/config#fetch)に引数を渡すことで最短0秒まで短くすることができますが、Firebaseの公式ドキュメントには

> https://firebase.google.com/docs/remote-config/android?hl=ja#caching_and_throttling
60 分の期間内でアプリは最大 5 回フェッチできます。それを超えると SDK によってスロットル処理が開始され、FirebaseRemoteConfigFetchThrottledException が返されます。

と書いてあるので注意が必要です。

## おわりに
もしかしたら今回紹介した以外にも良い方法があるかもしれません。
オススメの方法がありましたら、コメント等で教えていただけると嬉しいです。