ExpoのNotificationで指定したページに遷移させる

ExpoのNotification機能について下記の記事で詳しく検証しました。

Expo/Notificationの各機能を使ってみる
https://qiita.com/mildsummer/items/9dbfac3bf2ef857ca74f

この記事では、アプリ側で通知に対応する処理を掘り下げ、
通知をタップしたらアプリ内の特定のページを開くというような処理を実装してみます。
やり方は色々あると思うので、あくまで例として簡単なサンプルコードを用意し説明していきます。

# トークンを取得する

トップページ、および`A`、`B`、`C`のサブページがあり、通知に反応していずれかのサブページに遷移するというような構造を作っていきます。

まずはトップのページでExpoPushTokenを取得します。
本来ならDBなどに保存するかと思いますが、サンプルなので、コンソールに表示してPC側でコピーできるようにします。

```jsx:App.js
import React, { Component } from "react";
import { Text, View, Alert } from "react-native";
import { Notifications } from "expo";
import * as Permissions from "expo-permissions";

class Top extends Component {
  state = {
    token: null
  };

  componentDidMount() {
    this.getToken();
  }

  // トークンを取得
  getToken = async () => {
    const { status: existingStatus } = await Permissions.getAsync(
      Permissions.NOTIFICATIONS
    );
    let finalStatus = existingStatus;

    if (existingStatus !== "granted") {
      const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
      finalStatus = status;
    }
    if (finalStatus !== "granted") {
      return;
    }
    const token = await Notifications.getExpoPushTokenAsync();

    console.log("[EXPO NOTIFICATION TOKEN]", token);
    console.log(
      `Try running the command "node pushNotification.js ${
        token.match(/\[(.+)]/)[1]
      } [SCREEN NAME(A|B|C) or URL] [ID]"`
    );
    console.log(
      `ex) $ node pushNotification.js ${token.match(/\[(.+)]/)[1]} A 123`
    );
    this.setState({ token });
  };

  render() {
    const { token } = this.state;
    return (
      <View
        style={{
          flex: 1,
          width: "100%",
          justifyContent: "center",
          alignItems: "center"
        }}
      >
        <Text>{token || "getting token..."}</Text>
        <Text>Please look your console</Text>
      </View>
    );
  }
}
```

<img width="375" alt="PNGイメージ.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/c326242f-74f6-e269-1564-8e9914faf784.jpeg">

トークンを取得して表示するだけのトップページができました。

# React Navigation Stackでページ構造を準備

通知の内容に応じてページ遷移させたいので、`react-nativetion`と`react-navigation-stack`を使ってページの構造を準備しておきます。
サンプルとして、先ほどのトップの他に同じようなサブページ`A`、`B`、`C`をまとめて生成してしまいます。

```jsx:App.js
import { createAppContainer } from "react-navigation";
import { createStackNavigator } from "react-navigation-stack";

...

const stackOptions = {
  top: {
    screen: Top,
    navigationOptions: {
      header: null
    }
  }
};

// 同じようなサブページをまとめて作る
[
  { routeName: "A", color: "#6200EE" },
  { routeName: "B", color: "#03DAC6" },
  { routeName: "C", color: "#B00020" }
].forEach(option => {
  stackOptions[option.routeName] = {
    screen: props => (
      <View
        style={{
          flex: 1,
          width: "100%",
          justifyContent: "center",
          alignItems: "center",
          backgroundColor: option.color
        }}
      >
        <Text style={{ color: "#FFF", fontSize: 16 }}>screen name</Text>
        <Text style={{ color: "#FFF", fontSize: 24 }}>{option.routeName}</Text>
        <Text style={{ color: "#FFF", fontSize: 16, marginTop: 24 }}>ID</Text>
        <Text style={{ color: "#FFF", fontSize: 24 }}>
          {props.navigation.state.params.id || "-"}
        </Text>
      </View>
    ),
    navigationOptions: ({ navigation }) => ({
      headerTitle: `${option.routeName} ID:${navigation.state.params.id}`
    })
  };
});

const AppContainer = createAppContainer(
  createStackNavigator(stackOptions, {
    initialRouteName: "top"
  })
);

export default class App extends Component {
  render() {
    return (
      <AppContainer />
    );
  }
}
```

**[2020/04/04追記]**
`react-navigation`の最新版（v5）に合わせて書き換えた例を下記の記事で紹介しています。こちらはBare Workflowの`expo-notifications`を使用しています。
https://qiita.com/mildsummer/items/5acc267152449c86b2b1#%E7%94%BB%E9%9D%A2%E3%81%AE%E4%BD%9C%E6%88%90

# Push処理を実装

直接`curl`コマンド等を使ったりPostmanを使ったりでもいいのですが、
まず試しやすいように、Node.jsで通知を送信する処理を簡単に実装しました。

```js:pushNotification.js
const request = require("request");

const [, , token, routeName, id] = process.argv;

const isWebView = /http/.test(routeName);

request(
  {
    url: "https://expo.io/--/api/v2/push/send",
    method: "POST",
    json: {
      to: `ExponentPushToken[${token}]`,
      title: "通知サンプル",
      body: "ここに説明文が入ります",
      data: isWebView
        ? {
            url: routeName,
            params: { id }
          }
        : {
            routeName,
            params: { id }
          },
      _displayInForeground: false
    }
  },
  function(error, response, body) {
    if (error) {
      console.log(error);
    } else {
      console.log(body);
    }
  }
);
```

```console
$ node pushNotification.js [TOKEN] [SCREEN NAME(A|B|C) or URL] [ID]
```

引数に

* ExpoPushTokenのランダム文字列部分
* React Navigationの`routeName`にあたるスクリーン名あるいはWebViewで表示するURL
* 任意のID（React Navigationのスクリーンにパラメータとして渡す例として）

を渡せるようにしています。
`_displayInForeground`を`true`にすると、アプリ画面を開いている時にも通知が表示されるようになります。

# アプリ側で通知に対応する

Appコンポーネントに通知に対応する処理を追加します。

```js:App.js
import { NavigationActions, StackActions } from "react-navigation";
import { Notifications } from "expo";
import * as WebBrowser from "expo-web-browser";

export default class App extends Component {
  componentDidMount() {
    Notifications.addListener(notification => {
      console.log("received notification", notification);
      if (notification.origin === "selected") {
        this.respond(notification);
      } else if (notification.origin === "received") {
        Alert.alert("通知が届きました", "画面へ移動しますか？", [
          {
            text: "キャンセル",
            style: "cancel"
          },
          {
            text: "移動する",
            onPress: () => {
              this.respond(notification);
            }
          }
        ]);
      }
    });
  }

  respond(notification) {
    if (notification.data.url) { // URLの場合はWebViewを開く
      WebBrowser.openBrowserAsync(notification.data.url);
    } else if (this.navigator) { // サブページに遷移
      const actionOptions = {
        routeName: notification.data.routeName || "top",
        params: notification.data.params
      };
      if (this.navigator.state.nav.routes.length > 1) {
        // 現在もサブページの場合はスタック追加せず差し替える
        this.navigator.dispatch(StackActions.replace(actionOptions));
      } else {
        this.navigator.dispatch(NavigationActions.navigate(actionOptions));
      }
    }
  }

  render() {
    return (
      <AppContainer
        ref={ref => {
          this.navigator = ref;
        }}
      />
    );
  }
}
```

`Notification.addListener`でリスナー関数を追加して、Notificationインスタンスを取得します。
`origin`プロパティの詳しい挙動は[こちらの記事](https://qiita.com/mildsummer/items/9dbfac3bf2ef857ca74f#%E3%82%A2%E3%83%97%E3%83%AA%E5%81%B4%E3%81%A7%E9%80%9A%E7%9F%A5%E3%81%AB%E5%8F%8D%E5%BF%9C%E3%81%99%E3%82%8B)を参照してください。
`selected`の場合は、通知が表示され、タップされたことでアプリ画面に来た場合なので、直接画面を遷移させます。
`received`の場合はアプリを起動中に通知が届いた場合です。そのまま勝手に遷移させるのは良くないため、確認ダイアログを表示させるようにしました。
Notificationには任意の`data`プロパティが渡せますが、先ほどのスクリプトではURLを指定した場合`data.url`にURLを、それ以外の場合は`data.routeName`と`data.params.id`にスクリーン名とIDが入るようにしてあり、アプリ側ではその情報をみてWebViewを立ち上げるかReact Navigationのアクションをdispatchしてページ遷移させるようにします。

# 通知を送ってみる

まずはアプリを起動して、通知を許可したらターミナルかリモートデバッグのコンソールにトークンなどが表示されるかと思います。
トークンを使って先ほどのNode.jsスクリプトを叩くと、

```console
$ node pushNotification.js [TOKEN] A 123
```

アプリがバックグラウンドの時にはこのように通知が表示されます。（起動中の場合はダイアログが表示されます）
<img width="375" alt="PNGイメージ 11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/167ef507-5ceb-88df-f95d-db97f393aeca.png">

通知をタップすると
<img width="375" alt="スクリーンショット 2019-12-26 20.42.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/a77d2a8b-4adc-5439-1264-3f7fd908fcb7.png">

このようにアプリを開いたらすぐに指定したページへ遷移されます。

また、このようにURLを指定した場合

```console
$ node pushNotification.js [TOKEN] https://google.com
```

WebViewが起動し、指定したページが表示されます。
<img width="375" alt="PNGイメージ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/46eac015-4243-4736-72ea-4ec98d13f54d.png">

# Github

使用したソースコードはこちら。
https://github.com/mildsummer/expo-notification-example