結論
こちらのドキュメントを見ながら実装したところ、iOSでは驚くほど簡単に実装できました。
ただし、
https://docs.expo.dev/push-notifications/sending-notifications/ より引用
の通り、アプリケーションバッジに関しては、Android非対応の模様。
プッシュ通知の受信、通知をタップした際のイベントの登録事態はAndroidでも動作しました。
ことの発端
僕はかつて、アプリを開くことなく、千葉ロッテマリーンズの試合結果がわかるアプリ、chibadge(チバッジ)を開発運用していました。
しかし、アップデート申請がリジェクトされ続けるため、現在は泣く泣くストアから取り下げていたのです。
が。ここ2年、千葉ロッテマリーンズの調子が非常に良い。(2年連続2位)
ということで、chibadgeを復活させるべく、アプリの改修を始めようと思ったのですが、当時開発に使っていたツールが終了していました。。
通知を受け取るだけのアプリなので、Swiftで書き直してもよかったのですが、これを機に最近気になっていたExpoを調べてみることにしてみた次第です。
Expoの調査
ExpoはReact Nativeをつかったネイティブアプリ開発をサポートするフレームワークです。
導入の決め手は、
・普段からReactのフレームワークであるNext.jsを使っているので、Reactの記法には慣れている
・プッシュ通知の証明書が不要(Expoが用意してくれている)
・Androidアプリも同時につくれる ※ のちにアプリケーションバッジに対応していないことを知る
・開発時はホットリロードが効く
の4点(のちに3点)です。
特に、プッシュ通知は証明書周りが面倒という記憶があり、それが不要というのは非常にありがたかったです。
Expoの導入
❸ expo-cliの導入
yarn add -D expo-cli
で、導入しました。
本当はグローバルにインストールした方が便利だとは思うのですが、いつもなるべくローカルに導入しています。
❹ expo-cliにログイン
package.jsonのscriptsに、
"scripts": { "login": "expo login" }
と記載し、
yarn run login
でログインします。
僕はログインしてなかったので、通知を送ろうとした際に400が返ってきました。
参考) https://github.com/expo/expo/issues/9617
プッシュ通知を受信するアプリの作成
❶ プロジェクトの作成
package.jsonのscriptsに、
"scripts": { "init": "expo init" }
と記載し、
yarn run init
でプロジェクトを作成します。
TypeScriptを使いたかったので、Managed workflowのblank(TypeScript)でプロジェクトを作りました。
そして、プロジェクト作成後、作成されたディレクトリの中身をトップにまるまる移動しています。
❷ expo-notificationsの導入
package.jsonのscriptsに、
"scripts": { "install-notifications": "expo install expo-notifications", }
と記載し、
yarn run install-notifications
で、expo-notificationsを導入します。
❸ app.jsonを編集
app.jsonに、
"android": { "useNextNotificationsApi": true }
を追記します。これを書いておかないと、Android端末でプッシュ通知をタップした際の処理が効かないです。
❹ コードを書く
App.tsx
import Constants from 'expo-constants'; import * as Notifications from 'expo-notifications'; import React, { useState, useEffect, useRef } from 'react'; import { Button, Linking, StyleSheet, View } from 'react-native'; import { Subscription } from 'expo-modules-core'; const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center' }, }); Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: true, }), }); async function registerForPushNotificationsAsync() { let token; 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; } try { token = (await Notifications.getExpoPushTokenAsync()).data; } catch (err) { console.error(err); } } else { alert('Must use physical device for Push Notifications'); } return token; } export default function App() { const [ expoPushToken, setExpoPushToken ] = useState(''); const notificationListener = useRef<Subscription>(); const responseListener = useRef<Subscription>(); useEffect(() => { registerForPushNotificationsAsync().then((token) => token && setExpoPushToken(token)); responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => { // ❷ alert('yeah, yeah!'); console.log(response); }); return () => { notificationListener.current && Notifications.removeNotificationSubscription(notificationListener.current); responseListener.current && Notifications.removeNotificationSubscription(responseListener.current); }; }, []); return ( <View style={ styles.container }> <Button title="display token" onPress={() => { Linking.openURL(`https://kimizuka.fm?token=${ expoPushToken }`) }} // ❸ /> <Button title="clear badge" onPress={() => {{ Notifications.setBadgeCountAsync(0) }}} /> </View> ); }
ほぼほぼ、ドキュメントのコードなのですが、TypeScriptで書いたので所々書き方が異なっています。
また、ローカルプッシュは必要なかったので、該当箇所は削除しました。
ポイント
- TypeScriptだと、Constants.isDeviceは非推奨と商事されるのですが、ドキュメント上は非推奨になっておらず、型とドキュメントのどちらが間違っているかわからない、かつ、代替手段がわからないのでそのまま使っています。
- 通知をタップした際の処理を書いていますが、Android端末では app.json に "useNextNotificationsApi": true を記載しないと動作しません。
- tokenをコピペできるように、適当なURLのパラメーターにくっつけて外部ブラウザを起動するようにしてみました。
これでアプリは完成です。
挙動としては、
- display tokenをタップすると外部ブラウザが開く(?token=以降にトークンが表示される)
- プッシュ通知を受け取って、通知をタップすると「yeah, yeah!」と表示されるアプリです。
- clear badgeをタップするとバッジが消える(のちにiOSのみの機能ということを知る)
を実装しました。
タップした時に表示する文字列を「yeah, yeah!」にした理由はのちに判明します。
次に通知送信用のコードを書きます。
プッシュ通知を送信するサーバの作成
❶ node-fetchの導入
yarn add -D node-fetch
で、node-fetchを導入します。
❷ トークンをメモする
作成したアプリの「display token」をタップして、トークンをメモします。
❸ コードを書く
push.mjs
import fetch from 'node-fetch'; // ❶ const tokens = [ 'ExponentPushToken[XXXXXX]', 'ExponentPushToken[XXXXXX]' ]; tokens.forEach(async (token) => { const pushMessage = { to: token, sound: 'default', title: '本気で Push! Push!', badge: 0 | Math.random() * 9 + 1, // ❷ body: 'We can say!', data: { timestamp: Date.now() } } const res = 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) }); console.log(res); });
ポイント
- ファイル名を.mjsにすることでimportを使えるようにする 参考) https://blog.kimizuka.org/entry/2021/09/10/223310
- バッジに表示する数は1〜9をランダムで抽選する
という感じです。
これで、
node push.mjs
で、プッシュ通知が送れるようになりました。
タイトルに「本気で Push! Push!」、本文に「We can say!」、タップすると「yeah, yeah!」と表示されるはずです。
おわりに
当初の目標であった、「Expoでつくったアプリにローカルサーバからプッシュ通知を送ってアプリケーションバッジをつける」は、iOSアプリでしか達成できなかったのですが、Expoの導入から通知の受け取りまでは、iOS、Android共に達成できたので、まあ良しとしましょう。
開発用、本番用を分けて証明書を作るのが面倒だと思っていたのですが、Expoを使えば証明書を気にすることなく実装できることがわかったので、積極的に使っていきたいと思います。