React Native (Expo)で通知を送る

二つの通知のタイプがあります:

- **ローカル通知**：デバイスで設定されている
- **外部通知**：サーバーから来る

**ローカル通知**のユースケース: 
- 事前に設定することができる通知
- リマインダー、締め切り等
- 決済が完了した通知

**外部通知**のユースケース:
- チャットメッセージが届く時
- お知らせメッセージ

➤ このチュートリアルでは、ローカル通知も外部通知も送っていきます。手順は次のとおりです:

１. Expoプッシュトークンを取得する
２. アプリで通知の受け取りを設定する
３. テスト通知を送る

## 準備

Expoプロジェクトを作成しましょう。Visual Studio CodeなどのIDEで`expo init expo-push-notifications`を実行します。ターミナルで「blank template」を選びます。
必要なパッケージをインストールします： `expo install expo-notifications expo-device`を実行し、そうしたら`cd expo-push-notifications`、 `npm start`を実行します。 Expo開発環境の設定についてもっと知りたいなら、この[記事](https://qiita.com/Code-Kat/items/079e3b409b295ed423db#%E6%BA%96%E5%82%99)をお読みください。

## トークンの取得

App.jsxのコードをすべてを削除し、以下のコードに置き換えます。

```App.jsx
import { Button, Platform, Text, View } from "react-native";
import * as Notifications from "expo-notifications";
import * as Device from "expo-device";
import { useEffect, useState, useRef } from "react";

export default function App() {
　//プッシュトークンのバリュー
  const [expoPushToken, setExpoPushToken] = useState("");

　//プッシュ通知に登録する
  const registerForPushNofications = async () => {
    try {
　　　//アプリを実行しているデバイスはモバイルかどうかを確認する
      if (!Device.isDevice) {
        alert("このアプリはモバイルデバイスのみで実行できます");
        return;
      }
　　　//アプリは通知を受ける許可があるかどうかを確認する
      const { status: existingStatus } =
        await Notifications.getPermissionsAsync();
      let finalStatus = existingStatus;
　　　//まだ許可されていない場合には、許可を求める
      if (existingStatus !== "granted") {
        const { status } = await Notifications.requestPermissionsAsync();
        finalStatus = status;
      }
　　　//許可は否定されたら、ユーザに知らせる
      if (finalStatus !== "granted") {
        alert("プッシュ通知のトークンの取得ができませんでした。");
        return;
      }
　　　　
　　　//トークンを取得して、スマホの画面で表示する
      const token = (await Notifications.getExpoPushTokenAsync()).data;
      console.log("token", token);
      setExpoPushToken(token);

      //アンドロイドなら、通知のオプションを変更することができます。
      //vibrationPatternとlightColorを変更してみてください！
      if (Platform.OS === "android") {
        Notifications.setNotificationChannelAsync("default", {
          name: "default",
          importance: Notifications.AndroidImportance.MAX,
          vibrationPattern: [0, 250, 250, 250],
          lightColor: "#FF231F7C",
        });
      }
    } catch {
      (err) => alert(err);
    }
  };

  useEffect(() => {
    registerForPushNofications();
    };
  }, []);

  return (
    <View
      style={{
        flex: 1,
        alignItems: "center",
        justifyContent: "space-around",
      }}
    >
      <Text>あなたのExpoプッシュトークン: {expoPushToken}</Text>
    </View>
  );
}
```

### デバッグ
#### Notifications.getPermissionsAsync() is not a function
初めてコードを実行した時、expo-notificationsがインストールしていたのに、モバイルでNotificationsオブジェクトは空でありました。
同じエラーが発生すれば、次の手順に従って下さい： 

１． 開発中のアプリとExpo Goアプリを閉じて、IDEのターミナルでサーバーを切って、もう一回`npm start`を実行します。
２．問題が続ければ、`npm uninstall expo-notifications`,`npm i expo-notifications`を実行します。
３. 問題が続ければ、プロジェクトを削除して、再開します。新しいプロジェクトで`npm start`を実行する前に、スマホで、前のアプリとExpo Goアプリを閉じます。

#### console.logが機能しません

Expoで, コードの変更はスマホの画面で表示されるまで、時々何回ものセーブが必要です。時々、UIの変更が表示されるのに、console.logやalertが機能しません。この問題が発生すれば、次の手順に従って下さい：

1.　開発中のアプリとExpo Goアプリを閉じて、もう一回開く 
2. サーバーを切って、`npm start`をもう一度実行する
3. スマホの電源を切って、もうー同オンにする
 
## 通知受信の設定

アプリが通知を受け取る時の応答を設定しましょう。App.jsxに以下のコード追加します。

``` App.jsx
//ハンドラーを設定する
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    //通知を受け取る時に音が鳴らさないようにしたいなら、
    //shouldPlaySoundをfalseにして下さい。
    shouldPlaySound: false,
    shouldSetBadge: false,
  }),
});

export default function App() {
  ...
　//受け取った通知
  const [notification, setNotification] = useState('');
  const notificationListener = useRef();
  const responseListener = useRef();

  const registerForPushNofications = async () => {...};

  useEffect(() => {
    registerForPushNofications();
    
    //notificationを受け取った通知に設定する
    notificationListener.current =
      Notifications.addNotificationReceivedListener((notification) => {
        setNotification(notification);
      });
    
    //アプリがバックグラウンドにあり、ユーザーが通知をクリックした場合の対処方法
    responseListener.current =
      Notifications.addNotificationResponseReceivedListener((response) => {
        setNotification(response.notification);
        console.log(response.notification);
      });

    return () => {
      Notifications.removeNotificationSubscription(
        notificationListener.current
      );
      Notifications.removeNotificationSubscription(responseListener.current);
    };
  }, []);

  return (
    <View style={{...}}
    >
      <Text>あなたのExpoプッシュトークン: {expoPushToken}</Text>

      {/* 通知のタイトル、内容とデータを表示する */}
      <View style={{ alignItems: "center", justifyContent: "center" }}>
        <Text>
          タイトル: {notification && notification.request.content.title}{" "}
        </Text>
        <Text>内容: {notification && notification.request.content.body}</Text>
        <Text>
          データ:{" "}
          {notification && JSON.stringify(notification.request.content.data)}
        </Text>
      </View>
    </View>
  );
}
```

## テスト通知の送信

### ローカル通知
通知を送る関数と「送信」ボタンをApp.jsxに追加します。

``` App.jsx
...
  //ローカル通知お送る
  async function schedulePushNotification() {
    await Notifications.scheduleNotificationAsync({
      content: {
        title: "通知のタイトル! 📬",
        body: "通知の内容",
        data: { data: "通知のデータ" },
      },
      //通知が送信するまでに、何秒が掛かる
      trigger: { seconds: 2 },
    });
  }

  const registerForPushNofications = async () => {...};

...
      <Button
        title="通知を送信する"
        onPress={async () => {
          await schedulePushNotification();
        }}
      />
...
```
ボタンを押すと、通知が受け取ります！

### 外部通知
ExpoはPush notifications toolを提供します。

https://expo.dev/notifications

![Capture d’écran (149)_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2080708/a92ce8b9-c3f0-229e-5655-bc64ad7907f9.jpeg)

１．アプリを実行する時にIDEのターミナルで、token <あなたのトーケン>がログされています。そのトーケンをコピーして、ここでペストします
２．通知のタイトル（何でもいいです）
３．通知の内容（何でもいいです））
４．通知のデータ
５．通知が送信するまで、何秒が掛かる

それ以外の枠は入力しなくても大丈夫です。最後に「Send a Notification」ボタンをクリックして、スマホが通知を受け取ります。
![Capture d’écran (150).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2080708/0e86a2f0-a98e-01b9-37a5-f4f5eab0a007.png)

## まとめ

ローカル通知も外部通知も受信できるアプリを作りました！

完成したApp.jsx：
``` App.jsx
import { Button, Platform, Text, View } from "react-native";
import * as Notifications from "expo-notifications";
import * as Device from "expo-device";
import { useEffect, useState, useRef } from "react";

//ハンドラーを設定する
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    //通知を受け取る時に音が鳴らさないようにしたいなら、
    //shouldPlaySoundをfalseにして下さい。
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

export default function App() {
  //プッシュトークンのバリュー
  const [expoPushToken, setExpoPushToken] = useState("");
  //受け取った通知
  const [notification, setNotification] = useState("");
  const notificationListener = useRef();
  const responseListener = useRef();

  //ローカル通知お送る
  async function schedulePushNotification() {
    await Notifications.scheduleNotificationAsync({
      content: {
        title: "通知のタイトル! 📬",
        body: "通知の内容",
        data: { data: "通知のデータ" },
      },
      //通知が送信するまで、何秒が掛かる
      trigger: { seconds: 2 },
    });
  }

  //プッシュ通知に登録する
  const registerForPushNofications = async () => {
    try {
      //アプリを実行しているデバイスはモバイルかどうかを確認する
      if (!Device.isDevice) {
        alert("このアプリはモバイルデバイスのみで実行できます");
        return;
      }

      //アプリは通知を受ける許可があるかどうかを確認する
      const { status: existingStatus } =
        await Notifications.getPermissionsAsync();
      let finalStatus = existingStatus;
      //まだ許可されていない場合には、許可を求める
      if (existingStatus !== "granted") {
        const { status } = await Notifications.requestPermissionsAsync();
        finalStatus = status;
      }
      //許可は否定されたら、ユーザに知らせる
      if (finalStatus !== "granted") {
        alert("プッシュ通知のトークンの取得ができませんでした。");
        return;
      }

      //トークンを取得して、スマホの画面で表示する
      const token = (await Notifications.getExpoPushTokenAsync()).data;
      console.log("token", token);
      setExpoPushToken(token);

      //アンドロイドなら、通知のオプションを変更することができます。
      //vibrationPatternとlightColorを変更してみてください！
      if (Platform.OS === "android") {
        Notifications.setNotificationChannelAsync("default", {
          name: "default",
          importance: Notifications.AndroidImportance.MAX,
          vibrationPattern: [0, 250, 250, 250],
          lightColor: "#FF231F7C",
        });
      }
    } catch {
      (err) => alert(err);
    }
  };

  useEffect(() => {
    registerForPushNofications();

    //notificationを受け取った通知に設定する
    notificationListener.current =
      Notifications.addNotificationReceivedListener((notification) => {
        setNotification(notification);
      });

    //受け取ったデータをログする
    responseListener.current =
      Notifications.addNotificationResponseReceivedListener((response) => {
        console.log(response);
      });

    return () => {
      Notifications.removeNotificationSubscription(
        notificationListener.current
      );
      Notifications.removeNotificationSubscription(responseListener.current);
    };
  }, []);

  return (
    <View
      style={{
        flex: 1,
        alignItems: "center",
        justifyContent: "space-around",
      }}
    >
      <Text>あなたのExpoプッシュトークン: {expoPushToken}</Text>

      {/* 通知のタイトル、内容とデータを表示する */}
      <View style={{ alignItems: "center", justifyContent: "center" }}>
        <Text>
          タイトル: {notification && notification.request.content.title}{" "}
        </Text>
        <Text>内容: {notification && notification.request.content.body}</Text>
        <Text>
          データ:{" "}
          {notification && JSON.stringify(notification.request.content.data)}
        </Text>
      </View>
      <Button
        title="通知を送信する"
        onPress={async () => {
          await schedulePushNotification();
        }}
      />
    </View>
  );
}

```
## 参考　

### Firebase Cloud Messaging (アンドロイド)
Expo Goアプリを使わずにアンドロイドアプリを実行したいなら、Firebase Cloud Messagingが必要です。 アプリをApp Storeにアップロードする場合は、この点に注意してください。Firebase Cloud Messagingの設定方法： [ドキュメンテーション (英語）](https://docs.expo.dev/push-notifications/using-fcm/)　, [記事（日本語）](https://qiita.com/Code-Kat/private/85a1b83ca181cff4e8cd)。

### バックエンド

ローカル通知のみを使用する場合を除いて、通知の送信ができるバックエンドを設定するのが必要です。そのバックエンドは、下のように、Expo Notifications APIにPOSTリクエストができる必要があります。
```
fetch('https://exp.host/--/api/v2/push/send', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Accept-Encoding': 'gzip, deflate',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: <Expoトーケン>,
    data: { data: 'データ' },
    title: 'タイトル',
    body: '内容',
  }),
})
```

これはこのチュートリアルの範囲外ですが、いくつかのオプションがあります：
- [One Signal](https://documentation.onesignal.com/docs/react-native-expo-sdk-setup)
- Next.jsでサーバーを作る　（[この](https://qiita.com/Code-Kat/items/d5dbec5a2e8f39c9c0ad)記事を参考して下さい）