こんにちは、株式会社カミナシのエンジニア @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通知が可能になります。 自前でサーバーを用意することがないので非常に楽ちんです。
早速使ってみよう
大まかな流れは以下の通りです
- 通知の権限、Pushトークンの取得
- 通知したい内容を登録する
- アプリケーション側で通知内容を受け取る
通知の権限、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"}}%
他にもスケジュール通知もあるので詳しくはこちらを確認してみてください。
アプリケーション側で通知内容を受け取る
// アプリがフォアグラウンドの状態で通知を受信したときに起動
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通知では実現できない場合に検討するほうが良さそうです。
おわりに
expo-notificationsを使えば簡単にプッシュ通知が実装できました!Managed Workflowの場合は簡単に実装出来るので便利ですね。 次回は他のPackageで面白そうなものを紹介出来ればと思います。
最後に弊社ではエンジニアを募集しております。 興味がある、話を聞いてみたい、応募したいという方はお気軽にご応募ください!