wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

アプリの強制アップデート機能を作成

Firestoreの新設計を適応させるためにアプリの強制アップデート機能が必須になりそうだったので、アプリの強制アップデート機能を実装しました。

https://github.com/wheatandcat/PeperomiaTool/pull/8

これをv2.0.6で実装して、Firestoreの新設計をv3.0.0で実装する流れで行く予定です。

Pull Request

github.com

実装

まず、サポートするバージョンを指定します。

最初は、Firebase Remote Configでの値管理を想定していたのですが、現状Expoでは使えないみたいなので断念。

firebase.google.com

なので、Firestoreで値を管理するようにしました。 まずはsupportVersionのコレクションを作成してドキュメントにsupportVersion: 2.0.5を設定。 指定したバージョン未満の場合は強制アップデートするような実装にしていきます。

f:id:wheatandcat:20200921003918p:plain

次にアプリの方で、上記の値を取得できるように修正

■ src/containers/Version.tsx

import React, {
  useState,
  useEffect,
  createContext,
  useCallback,
  useContext,
} from 'react';
import AsyncStorage from '@react-native-community/async-storage';
import compareVersions from 'compare-versions';
import { getFireStore } from 'lib/firebase';
import { getSupportVersion } from 'lib/firestore/supportVersion';
import ForceUpdate from 'components/pages/ForceUpdate/Page';

export const Context = createContext<ContextProps>({});
const { Provider } = Context;

export type ContextProps = Partial<{
  onCheckForceUpdate: () => Promise<void>;
}>;

type Props = {};

type State = {
  forceVersionUpdate: boolean;
};

const Version: React.FC<Props> = (props) => {
  const [state, setState] = useState<State>({
    forceVersionUpdate: false,
  });

  useEffect(() => {
    onCheckForceUpdate();
  });

  const onCheckForceUpdate = useCallback(async () => {
    let appVersion = await AsyncStorage.getItem('APP_VERSION');
    if (!appVersion) {
      appVersion = '1.0.0';
    }

    const firestore = getFireStore();
    const supportVersion = await getSupportVersion(firestore);

    if (compareVersions.compare(supportVersion, appVersion, '>')) {
      setState((s) => ({
        ...s,
        forceVersionUpdate: true,
      }));
    }
  }, []);

  if (state.forceVersionUpdate) {
    return <ForceUpdate />;
  }

  return <Provider value={{ onCheckForceUpdate }}>{props.children}</Provider>;
};

export default Version;

export const useVersion = () => useContext(Context);
export const Consumer = Context.Consumer;

内容的には、以下の部分でFirestoreからバージョンを取得してアプリのバージョンと比較して、未満の場合は、forceVersionUpdateをtrueに変更します。 forceVersionUpdate = trueの場合は、強制アップデートの表示を行います。

  const onCheckForceUpdate = useCallback(async () => {
    let appVersion = await AsyncStorage.getItem('APP_VERSION');
    if (!appVersion) {
      appVersion = '1.0.0';
    }

    const firestore = getFireStore();
    const supportVersion = await getSupportVersion(firestore);

    if (compareVersions.compare(supportVersion, appVersion, '>')) {
      setState((s) => ({
        ...s,
        forceVersionUpdate: true,
      }));
    }
  }, []);

  if (state.forceVersionUpdate) {
    return <ForceUpdate />;
  }

また上記の設定だとはバージョンの比較は、アプリ起動時のみになってしまうので、 アプリがバックグラウンドからフォアグラウンドに変わった瞬間にも判定を行うようにします。

import React, { useCallback, useEffect, useState } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import { useVersion } from './Version';

type Props = {};

type State = {
  loading: boolean;
};

const AppStateContainer: React.FC<Props> = (props) => {
  const [appState, setAppState] = useState<AppStateStatus>(
    AppState.currentState
  );
  const { onCheckForceUpdate } = useVersion();

  const check = useCallback(async () => {
    await onCheckForceUpdate?.();
  }, [onCheckForceUpdate]);

  const handleAppStateChange = useCallback(
    async (nextAppState) => {
      if (appState.match(/inactive|background/u) && nextAppState === 'active')
        await check();

      setAppState(nextAppState);
    },
    [appState, check]
  );

  useEffect(() => {
    AppState.addEventListener('change', handleAppStateChange);

    return () => {
      AppState.removeEventListener('change', handleAppStateChange);
    };
  }, [handleAppStateChange]);

  return <>{props.children}</>;
};

export default AppStateContainer;

上記のAppStateを使用する事でバックグラウンドからフォアグラウンドのイベントを取得できるので、その時に判定を行うように実装しています。

これで以下のタイミングで強制アップデート判定を行うようになりました。 - アプリ起動時 - アプリがバックグラウンドからフォアグラウンドに変わった時

最後に強制アップデート画面の実装です。

■ src/components/pages/ForceUpdate/Page.tsx

import React, { useCallback } from 'react';
import { View, ScrollView, Text, SafeAreaView } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
import AppLink from 'react-native-app-link';
import { Button } from 'react-native-elements';

type Props = {};
const appStoreId = 1460583871;
const playStoreId = 'com.wheatandcat.peperomia';

const ForceUpdate: React.FC<Props> = () => {
  const onPress = useCallback(() => {
    AppLink.openInStore({
      appName: 'シェアフル',
      appStoreId,
      appStoreLocale: 'jp',
      playStoreId,
    });
  }, []);

  return (
    <SafeAreaView>
      <View style={styles.root}>
        <ScrollView>
          <View style={styles.inner}>
            <View style={styles.textContainer}>
              <Text style={styles.title}>
                最新のバージョンのアプリを{'\n'}インストールしてください。
              </Text>
            </View>
            <View style={styles.buttonContainer}>
              <Button title="ストアへ移動する" onPress={onPress} />
            </View>
          </View>
        </ScrollView>
      </View>
    </SafeAreaView>
  );
};

export default ForceUpdate;

const styles = EStyleSheet.create({
  root: {
    backgroundColor: '$background',
    height: '100%',
  },
  inner: {
    height: '100%',
    paddingHorizontal: 15,
    paddingVertical: 15,
  },
  title: {
    color: '$text',
    fontSize: 16,
    fontWeight: 'bold',
    textAlign: 'center',
  },

  textContainer: {
    paddingTop: 15,
    paddingBottom: 15,
  },
  buttonContainer: {
    paddingHorizontal: 15,
    paddingVertical: 15,
  },
});

react-native-app-linkを使うことで簡単にストアへのリンクを作成できれるので、こちらストアに飛ばすように修正。

github.com

これで、以下のように表示されます。

これで準備が完了したので、次はv3.0の更新想定についての記事を作成しようと思います。