カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

【React Native + Expo】Push通知を試してみた

こんにちは、株式会社カミナシのエンジニア @imuです。

はじめに

と、いうことでPush通知の話です!

カミナシではReact Native(Expo)でアプリケーションを開発しています。

Push通知の実装って意外と難しいイメージありませんか? アプリケーション側での監視、通知内容を保存しておくサーバー、どういうタイミングで通知するかのパターン等を考えると、簡単なパッケージがあれば楽なのに…と思ってしまいます。

Expoには便利なPackageがいくつかあって、通知のPackageもあります! どれくらい簡単に実装できるか、ご紹介出来ればと思います。

『カミナシ』のアプリケーションにはまだ実装されていないのですが、これくらい簡単であれば入れたいな…。

環境構築

ExpoCLIのインストール
npm install -g expo-cli 

インストールが終わったらひな形を作成しましょう。 今回はManaged workflowのblank(TypeScript)を利用します。

expo init notification
通知のパッケージをインストール

公式ドキュメントに従って通知のパッケージをインストールします。 BareWorkflowを使った場合はこちらを参照してください。

expo install expo-notifications

Expoのサーバーを利用してPush通知が可能になります。 自前でサーバーを用意することがないので非常に楽ちんです。

docs.expo.io

早速使ってみよう
大まかな流れは以下の通りです
  1. 通知の権限、Pushトークンの取得
  2. 通知したい内容を登録する
  3. アプリケーション側で通知内容を受け取る
通知の権限、Pushトークンの取得
if (Constants.isDevice) {
  // 通知の権限を確認します
  const { status: existingStatus } = await Notifications.getPermissionsAsync()
  let finalStatus = existingStatus
  if (existingStatus !== 'granted') {
    // 通知の権限がない場合は、再度通知の権限を確認します
    const { status } = await Notifications.requestPermissionsAsync()
    finalStatus = status
  }
  if (finalStatus !== 'granted') {
    alert('Failed to get push token for push notification!')
  } else {
    // Pushトークンの取得 
    const token = (await Notifications.getExpoPushTokenAsync()).data
  }
} else {
  // Push通知はSimulatorでは確認できない
  alert('Must use physical device for Push Notifications')
}
通知したい内容を登録する
// 通知したい内容をRequestBodyに設定する
const pushMessage = {
  // Push通知のトークン
  to: expoPushToken,
  // Push通知の音を設定
  sound: 'default',
  // Push通知のタイトル
  title: title,
  // Push通知の内容
  body: body,
  // dataは実際にPush通知には表示されないがアプリケーション側で参照することが出来るので、アプリケーション側になにかさせる場合は設定すると良さそう
  data: { addData: data, sample: 'test' }
}

await 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(pushMessage)
})

fetch関数を呼び出しているだけなので、コマンドラインからcurlを実行しても良いです。

% curl -H "Content-Type: application/json" -X POST "https://exp.host/--/api/v2/push/send" \                                                                                                    (git)-[main]
> -d '{"to":"ExponentPushToken[Pushしたい先のトークン]","title":"kaminashi","body":"blog"}'
{"data":{"status":"ok","id":"hoge"}}%

他にもスケジュール通知もあるので詳しくはこちらを確認してみてください。

docs.expo.io

アプリケーション側で通知内容を受け取る
// アプリがフォアグラウンドの状態で通知を受信したときに起動
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
  // notificationには通知内容が含まれています
  setNotification(notification)
})

// ユーザーが通知をタップまたは操作したときに発生します
// (アプリがフォアグラウンド、バックグラウンド、またはキルされたときに動作します)
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
  console.log(response)
})
最終的なコード

公式サイトのサンプルを元に、画面から入力した内容でPush通知できるように変更したものがこちらです。

import Constants from 'expo-constants'
import * as Notifications from 'expo-notifications'
import React, { useState, useEffect, useRef } from 'react'
import { Text, View, Button, Platform, TextInput, StyleSheet, TouchableHighlight } from 'react-native'
import { Subscription } from '@unimodules/core'

export default function App() {
  const [expoPushToken, setExpoPushToken] = useState('')
  const [notification, setNotification] = useState<Notifications.Notification>()
  const notificationListener = useRef<Subscription>()
  const responseListener = useRef<Subscription>()
  const [title, onChangeTitle] = useState<string>('')
  const [body, onChangeBody] = useState<string>('')
  const [data, onChangeData] = useState<string>('')

  useEffect(() => {
    // 通知を受信した時の振る舞いを設定
    Notifications.setNotificationHandler({
      handleNotification: async () => ({
        shouldShowAlert: true,
        shouldPlaySound: false,
        shouldSetBadge: false
      })
    })

    // Expo Pushトークンを取得
    registerForPushNotificationsAsync().then(token => token && setExpoPushToken(token))

    // アプリがフォアグラウンドの状態で通知を受信したときに起動
    notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
      setNotification(notification)
    })

    // ユーザーが通知をタップまたは操作したときに発生します
    // (アプリがフォアグラウンド、バックグラウンド、またはキルされたときに動作します)
    responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
      alert('ユーザーが通知をタップしました')
      console.log(response)
    })

    // アンマウント時にリスナーを削除
    return () => {
      const notification = notificationListener.current
      notification && Notifications.removeNotificationSubscription(notification)
      const response = responseListener.current
      response && Notifications.removeNotificationSubscription(response)
    }
  }, [])

  return (
    <View style={styles.container}>
      <Text style={styles.title}>プッシュ通知を試してみよう🙌</Text>
      <View style={styles.pushView}>
        <Text style={styles.title}>プッシュする内容を入力📤</Text>
        <TextInput
          style={styles.input}
          placeholder='タイトルを入力してください'
          value={title}
          onChangeText={onChangeTitle}
        />
        <TextInput
          style={styles.input}
          placeholder='メッセージを入力してください'
          value={body}
          onChangeText={onChangeBody}
        />
        <TextInput
          style={styles.input}
          placeholder='追加メッセージ(通知には表示されない)'
          value={data}
          onChangeText={onChangeData}
        />
      </View>
      <View style={styles.pushView}>
        <Text style={styles.title}>プッシュした内容を表示📥</Text>
        <View style={styles.output}>
          <Text style={styles.outputText}>タイトル: {notification && notification.request.content.title}</Text>
          <Text style={styles.outputText}>メッセージ: {notification && notification.request.content.body}</Text>
          <Text style={styles.outputText}>
            追加メッセージ: {notification && JSON.stringify(notification.request.content.data)}
          </Text>
        </View>
      </View>
      <View style={styles.buttonView}>
        <TouchableHighlight style={styles.button}>
          <Button
            title='プッシュ通知'
            color='white'
            onPress={async () => {
              await sendPushNotification(expoPushToken, title, body, data)
            }}
          />
        </TouchableHighlight>
        <TouchableHighlight style={styles.button}>
          <Button
            title='スケジュールのプッシュ通知'
            color='white'
            onPress={async () => {
              await schedulePushNotification()
            }}
          />
        </TouchableHighlight>
      </View>
    </View>
  )
}

async function sendPushNotification(expoPushToken: string, title: string, body: string, data: string) {
  const pushMessage = {
    to: expoPushToken,
    sound: 'default',
    title: title,
    body: body,
    data: { addData: data, sample: 'test' }
  }

  await 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(pushMessage)
  })
}

async function schedulePushNotification() {
  await Notifications.scheduleNotificationAsync({
    content: {
      title: 'スケジュール通知 📬',
      body: 'スケジュール通知されました!'
    },
    trigger: {
      seconds: 2
    }
  })
}

async function registerForPushNotificationsAsync() {
  let token: string = ''

  if (Constants.isDevice) {
    const { status: existingStatus } = await Notifications.getPermissionsAsync()
    let finalStatus = existingStatus
    if (existingStatus !== 'granted') {
      const { status } = await Notifications.requestPermissionsAsync()
      finalStatus = status
    }
    if (finalStatus !== 'granted') {
      alert('Failed to get push token for push notification!')
      return
    }
    token = (await Notifications.getExpoPushTokenAsync()).data
  } else {
    alert('Must use physical device for Push Notifications')
  }

  if (Platform.OS === 'android') {
    Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C'
    })
  }

  return token
}

export const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'space-around'
  },
  pushView: {
    width: '100%',
    alignItems: 'center',
    justifyContent: 'center'
  },
  title: {
    fontWeight: 'bold',
    fontSize: 34,
    lineHeight: 49
  },
  input: {
    width: '80%',
    height: 40,
    margin: 12,
    borderWidth: 1
  },
  output: {
    width: '100%',
    alignItems: 'center',
    justifyContent: 'center'
  },
  outputText: {
    width: '80%',
    fontWeight: 'bold',
    fontSize: 18,
    lineHeight: 40,
    margin: 12,
    borderWidth: 1
  },
  buttonView: {
    width: '100%',
    alignItems: 'center',
    justifyContent: 'center'
  },
  button: {
    height: 40,
    width: '50%',
    borderRadius: 12,
    backgroundColor: 'gray',
    marginBottom: 24
  }
})

iPadOS 14.4の実機で画面収録したものがこちらになります。 入力した内容でプッシュ通知を行い、その後スケジュール通知(ボタン押下から2秒後に通知される)を行っています。 gyazo.com

APNs(Apple Push Notification Service), FCM(Firebase Cloud Messaging)を利用した通知の紹介

APNs / FCM を理解していて、ExpoのPush通知では実現できない場合に検討するほうが良さそうです。

docs.expo.io

おわりに

expo-notificationsを使えば簡単にプッシュ通知が実装できました!Managed Workflowの場合は簡単に実装出来るので便利ですね。 次回は他のPackageで面白そうなものを紹介出来ればと思います。

最後に弊社ではエンジニアを募集しております。 興味がある、話を聞いてみたい、応募したいという方はお気軽にご応募ください!

open.talentio.com