通知の文面を試そうと、Android の実機で Expo Go を開いた夜のことです。送信ツールの画面には「送信済み」と出ているのに、端末はいつまでも沈黙したままでした。
最初に疑ったのは端末の側でした。通知の許可を見直し、省電力モードを切り、Wi-Fi をモバイル回線に替えました。それでも届きません。気づけば 1 時間ほどが過ぎておりました。
原因は端末にも、送信の書き方にもありませんでした。Expo Go という道具そのものが、リモートプッシュを受け取れる作りではなくなっていたのです。昨日まで動いていた手順が今日は通らないとき、手元より先に道具の仕様を見る——この順番を、私はこの夜に決め直しました。
いま思えば、1 時間を溶かした本当の原因は、疑う順番を決めていなかったことだったのかもしれません。
同じところで手が止まっている方に向けて、確かめる順番と、development build へ移る最小の手順を書き残します。
最初にお伝えしたいのは「Android の Expo Go では、リモートプッシュは対象外」という点です
Expo のドキュメントでは、SDK 53 以降、Android の Expo Go ではリモートのプッシュ通知が使えない旨が案内されています。通知を本気で試したい場合は development build を使う、というのが公式の道筋です。
iOS の Expo Go については、私の手元では検証しておりません。ここは公式 FAQ の最新の記述を優先してください。バージョンごとに扱いが変わりうる箇所だからです。
大切なのは、「届かない」という症状が 3 つの別々の原因から生まれうることです。
| 症状 | 疑う場所 | 見分け方 |
|---|---|---|
| 送信ツールが成功を返すが、端末に何も出ない | 実行環境(Expo Go かどうか) | アプリ内で実行環境を表示して確かめる |
| トークンの取得で例外が出る | projectId/権限/Android の資格情報 | 例外メッセージの文言を読む |
| development build では届くが、本番ビルドだけ届かない | ビルドごとの資格情報(FCM/APNs) | EAS の credentials 画面で対象ビルドの設定を見る |
1 行目だけが、Expo Go に固有の症状です。残りの 2 行は development build に移ったあとにも出ますので、先に切り分けておく価値があります。
切り分け①:いま動いているのが Expo Go かどうかを、コードに言わせます
端末の見た目で判断すると、思い込みが入ります。アプリ自身に実行環境を答えさせるほうが確実です。
// ExecutionEnvironmentBadge.tsx
// 何を解決するか: 「いま Expo Go で動いているのか」を画面で確定させる
import Constants, { ExecutionEnvironment } from 'expo-constants';
import { Text, View } from 'react-native';
export function ExecutionEnvironmentBadge() {
// StoreClient が Expo Go を指します(bare / standalone と区別されます)
const isExpoGo =
Constants.executionEnvironment === ExecutionEnvironment.StoreClient;
return (
<View style={{ padding: 8 }}>
<Text>
実行環境: {isExpoGo ? 'Expo Go' : 'development build / 本番ビルド'}
</Text>
</View>
);
}なぜこう書くかというと、Constants.appOwnership のような古い判定は将来の版で消えることがあるからです。実行環境の列挙値で見ておけば、判定の根拠を公式の型に預けられます。
画面に「Expo Go」と出たなら、通知の設定を探し回る必要はありません。道具を替えるだけです。
切り分け②:トークン取得の例外は、握りつぶさずに画面へ出します
development build に移っても、トークンが取れない場面はあります。取得の関数は、失敗の理由を文言で教えてくれます。握りつぶすと、その文言を読み損ねます。
// registerForPush.ts
// 何を解決するか: 権限・projectId・Android チャンネルの3つを1か所で確認する
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
export async function registerForPush(): Promise<string> {
if (Platform.OS === 'android') {
// Android 8 以降は、チャンネルが無いと通知が表示されません
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.DEFAULT,
});
}
const current = await Notifications.getPermissionsAsync();
let status = current.status;
if (status !== 'granted') {
status = (await Notifications.requestPermissionsAsync()).status;
}
if (status !== 'granted') {
throw new Error('通知の許可が得られていません(端末の設定から許可が必要です)');
}
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
throw new Error('projectId が見つかりません(eas init を実行したか確認してください)');
}
const { data } = await Notifications.getExpoPushTokenAsync({ projectId });
return data; // ExponentPushToken[...] の形で返ります
}呼び出し側では try / catch で受けて、メッセージをそのまま画面に出してください。ログだけに流すと、実機を片手にターミナルへ視線を往復させることになります。手が止まる時間は、ここでいちばん長くなります。
Android の場合、トークンの取得そのものは通っても、実際の配信には FCM の資格情報が必要です。この設定は EAS の credentials から行います。
切り分け③:ローカル通知で、端末側の経路を先に確かめます
リモートプッシュは、Expo のサーバーと FCM/APNs を経由する長い道のりです。一方、端末内で予約するローカル通知は、その道のりを通りません。
// scheduleLocalProbe.ts
// 何を解決するか: 端末の許可とチャンネルが生きているかを、サーバー抜きで確認する
import * as Notifications from 'expo-notifications';
export async function scheduleLocalProbe() {
await Notifications.scheduleNotificationAsync({
content: { title: '通知の疎通確認', body: '5秒後に出れば端末側は健全です' },
trigger: {
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
seconds: 5,
},
});
}5 秒後に表示されれば、端末の許可とチャンネルは生きています。疑う範囲を「サーバーから先」に絞れます。表示されないなら、許可かチャンネルが先です。
確かめる場所は、近いほうから順に。 端末→アプリ→サーバーという順を守るだけで、迷う時間はずいぶん減りました。
development build に移る最小の手順
ここまでで「Expo Go が原因」と確定したら、移ります。一度作れば、日々の開発は Expo Go とほぼ同じ手触りで続けられます。
# 1) 開発用クライアントを入れます
npx expo install expo-dev-client
# 2) EAS にログインしてプロジェクトを紐づけます(未実施なら)
npx eas-cli login
npx eas-cli init
# 3) 開発用ビルドを作ります(Android 実機に入れる例)
npx eas-cli build --profile development --platform androideas.json には、開発用のプロファイルを足します。
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"android": { "buildType": "apk" }
},
"production": {}
}
}distribution: "internal" と buildType: "apk" の組み合わせにしているのは、ストアを通さずに実機へ直接入れられるからです。ビルドが終わると QR とリンクが出ますので、実機で開いてインストールします。
入れたあとの起動は、これまでと少しだけ違います。
# development build に接続する形で開発サーバーを起動します
npx expo start --dev-client通知以外にも、Expo Go では試せないものがあります
通知で一度詰まった方は、同じ壁が別の機能でも出ることを知っておくと楽です。Expo Go に同梱されていないネイティブモジュールを足したとき、あるいはアプリ固有のネイティブ設定(URL スキームや権限の文言など)を確かめたいときは、同じく development build が必要になります。
個人開発でアプリを何本も育てていると、「この機能は Expo Go で足りるのか」を最初に決めておくほうが、後の手戻りが小さいと感じます。私自身は、通知・課金・独自のネイティブ設定のどれかに触れる日は、最初から development build で始めております。
なお、ログイン周りで Expo Go が開かなくなった場合は、Expo Go がプレビューを開かなくなった日に、最初に確かめる2つのログインで別の切り分けをお伝えしています。
次の一歩
今日の作業は、1 つだけで十分です。ExecutionEnvironmentBadge を、使っているアプリの設定画面に 1 行足してみてください。画面に「Expo Go」と出たら、通知の設定を探すのをやめて、npx expo install expo-dev-client から始めていただければと思います。
届かない通知を前にして、疑う順番を先に決めておく。その線引きだけは、これからも守るようにしています。