expo-notifications
Permissions, channels, foreground handling, and push token registration - the expo-notifications cookbook for Expo SDK 57 apps that need local alerts and remote push.
Search across all documentation pages
Permissions, channels, foreground handling, and push token registration - the expo-notifications cookbook for Expo SDK 57 apps that need local alerts and remote push.
Quick-reference recipe card - copy-paste ready.
npx expo install expo-notifications expo-device expo-constants// src/notifications/setup.ts - import at app entry
import * as Notifications from "expo-notifications";
import { Platform } from "react-native";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export async function ensureAndroidChannel() {
if (Platform.OS !== "android") return;
await Notifications.setNotificationChannelAsync("default", {
name: "Default",
importance: Notifications.AndroidImportance.DEFAULT,
});
}
export async function requestNotificationPermission(): Promise<boolean> {
const { status: existing } = await Notifications.getPermissionsAsync();
if (existing === "granted") return true;
const { status } = await Notifications.requestPermissionsAsync();
return status === "granted";
}// src/notifications/pushToken.ts
import Constants from "expo-constants";
import * as Device from "expo-device";
import * as Notifications from "expo-notifications";
import { Platform } from "react-native";
export async function getExpoPushToken(): Promise<string | null> {
if (!Device.isDevice) return null;
const granted = await requestNotificationPermission();
if (!granted) return null;
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
throw new Error("EAS projectId missing - run npx eas init");
}
const token = await Notifications.getExpoPushTokenAsync({ projectId });
return token.data;
}When to reach for this:
When to avoid:
Permission gate, Android channel, token registration, and tap-to-navigate listener.
npx expo install expo-notifications expo-device expo-constants// app/(tabs)/settings/notifications.tsx
import { useState } from "react";
import { Pressable, Text, View } from "react-native";
import {
ensureAndroidChannel,
requestNotificationPermission,
} from "@/notifications/setup";
import { registerPushToken } from "@/notifications/registerPushToken";
export default function NotificationSettingsScreen() {
const [status, setStatus] = useState<"idle" | "granted" | "denied">("idle");
async function enableNotifications() {
await ensureAndroidChannel();
const granted = await requestNotificationPermission();
if (!granted) {
setStatus("denied");
return;
}
await registerPushToken();
setStatus("granted");
}
return (
<View style={{ padding: 24, gap: 12 }}>
<Text>Get alerts when drivers arrive on site.</Text>
<Pressable onPress={enableNotifications}>
<Text style={{ color: "#2563eb", fontWeight: "600" }}>
Enable notifications
</Text>
</Pressable>
{status === "denied" && (
<Text>Open Settings to allow notifications for this app.</Text>
)}
</View>
);
}// src/notifications/registerPushToken.ts
import { getExpoPushToken } from "./pushToken";
export async function registerPushToken() {
const token = await getExpoPushToken();
if (!token) return;
await fetch(`${process.env.EXPO_PUBLIC_API_URL}/devices/push-token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, platform: "expo" }),
});
}// app/_layout.tsx
import { useEffect } from "react";
import * as Notifications from "expo-notifications";
import { router, Stack } from "expo-router";
import "@/notifications/setup";
export default function RootLayout() {
useEffect(() => {
const sub = Notifications.addNotificationResponseReceivedListener(
(response) => {
const orderId = response.notification.request.content.data?.orderId;
if (typeof orderId === "string") {
router.push(`/orders/${orderId}`);
}
}
);
return () => sub.remove();
}, []);
return <Stack />;
}What this demonstrates:
getExpoPushTokenAsync.data.orderId.| Step | API | Notes |
|---|---|---|
| Check current | getPermissionsAsync() | undetermined on first launch |
| Explain value | In-app screen | Required for App Review narrative |
| Request | requestPermissionsAsync() | iOS shows system sheet once per install |
| Android 13+ | Same + channel | POST_NOTIFICATIONS runtime permission |
HIGH only for urgent alerts.scheduleNotificationAsync or remote display.| State | Behavior | Configuration |
|---|---|---|
| Foreground | Banner controlled by setNotificationHandler | shouldShowBanner: true |
| Background | OS displays per channel settings | Default handler still applies to data-only on Android |
| Killed | Tap opens app; use getLastNotificationResponseAsync | Cold-start navigation |
Install → getExpoPushTokenAsync → POST /devices
Reinstall → new token → upsert server row
Logout → DELETE server token + optional unregister
Account switch → register new token under new userIddeviceId, not only userId.getExpoPushTokenAsync - production apps often bridge to FCM/APNs directly via EAS credentials.| API | Purpose |
|---|---|
setNotificationHandler | Foreground presentation |
setNotificationChannelAsync | Android channel setup |
getExpoPushTokenAsync | Expo push token |
scheduleNotificationAsync | Local scheduled alerts |
addNotificationReceivedListener | Foreground receive |
addNotificationResponseReceivedListener | User tapped notification |
getLastNotificationResponseAsync | Cold-start tap handling |
getExpoPushTokenAsync throws. Fix: npx eas init and commit extra.eas.projectId.setNotificationChannelAsync before first post.index.ts or _layout.tsx top.data.token in push is interceptable. Fix: Exchange opaque IDs server-side.| Alternative | Use When | Don't Use When |
|---|---|---|
expo-notifications local | Scheduled reminders, offline alerts | Server-driven marketing at scale without backend |
| Expo push + EAS | Greenfield Expo apps | You already own FCM/APNs direct integration |
| FCM/APNs direct | Custom payload control | You want fastest Expo setup |
| In-app only (no push) | User is always in-app during task | Field workers who background the app |
npx expo install expo-notifications expo-device expo-constantsAdd the config plugin in app.config.ts for custom icons and sounds. Rebuild native binary after plugin changes.
Device.isDevice is false on simulators.const last = await Notifications.getLastNotificationResponseAsync();
if (last) navigateFromNotification(last);channelId in scheduleNotificationAsync content for local; remote payloads set channelId on Android.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026