Overview
Push notifications allow you to engage users even when your app isn’t running. Expo provides expo-notifications to handle both local and remote notifications across iOS and Android.
Installation
Install the package
npx expo install expo-notifications expo-device expo-constants
Configure your app.json
Add notification configuration to your app.json: {
"expo" : {
"plugins" : [
[
"expo-notifications" ,
{
"icon" : "./assets/notification-icon.png" ,
"color" : "#ffffff" ,
"sounds" : [ "./assets/notification-sound.wav" ]
}
]
],
"android" : {
"googleServicesFile" : "./google-services.json"
},
"ios" : {
"infoPlist" : {
"UIBackgroundModes" : [ "remote-notification" ]
}
}
}
}
Request permissions
Create a hook to manage notification permissions: hooks/useNotifications.ts
import { useState , useEffect , useRef } from 'react' ;
import * as Notifications from 'expo-notifications' ;
import * as Device from 'expo-device' ;
import { Platform } from 'react-native' ;
export function useNotifications () {
const [ expoPushToken , setExpoPushToken ] = useState < string >();
const [ notification , setNotification ] = useState < Notifications . Notification >();
const notificationListener = useRef < Notifications . Subscription >();
const responseListener = useRef < Notifications . Subscription >();
useEffect (() => {
registerForPushNotificationsAsync (). then ( token => {
setExpoPushToken ( token );
});
notificationListener . current = Notifications . addNotificationReceivedListener ( notification => {
setNotification ( notification );
});
responseListener . current = Notifications . addNotificationResponseReceivedListener ( response => {
console . log ( 'User tapped notification:' , response );
});
return () => {
notificationListener . current ?. remove ();
responseListener . current ?. remove ();
};
}, []);
return { expoPushToken , notification };
}
async function registerForPushNotificationsAsync () {
let token ;
if ( Platform . OS === 'android' ) {
await Notifications . setNotificationChannelAsync ( 'default' , {
name: 'default' ,
importance: Notifications . AndroidImportance . MAX ,
vibrationPattern: [ 0 , 250 , 250 , 250 ],
lightColor: '#FF231F7C' ,
});
}
if ( Device . 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' );
}
return token ;
}
Handling Notifications
Set how notifications are displayed when the app is foregrounded:
import * as Notifications from 'expo-notifications' ;
Notifications . setNotificationHandler ({
handleNotification : async () => ({
shouldShowAlert: true ,
shouldPlaySound: true ,
shouldSetBadge: true ,
}),
});
Listen for notifications
Foreground
Background/Quit
useEffect (() => {
const subscription = Notifications . addNotificationReceivedListener ( notification => {
console . log ( 'Notification received:' , notification );
// Update UI, show in-app notification, etc.
});
return () => subscription . remove ();
}, []);
useEffect (() => {
const subscription = Notifications . addNotificationResponseReceivedListener ( response => {
const { notification } = response ;
const data = notification . request . content . data ;
// Navigate based on notification data
if ( data . screen ) {
router . push ( data . screen );
}
});
return () => subscription . remove ();
}, []);
Sending Notifications
Local notifications
async function scheduleLocalNotification () {
await Notifications . scheduleNotificationAsync ({
content: {
title: "Time's up!" ,
body: 'Your timer has finished' ,
data: { screen: '/timer' },
sound: 'notification-sound.wav' ,
},
trigger: {
seconds: 60 ,
// Or use a specific date:
// date: new Date(Date.now() + 60 * 1000)
},
});
}
Push notifications from your server
const sendPushNotification = async ( expoPushToken , title , body , data ) => {
const message = {
to: expoPushToken ,
sound: 'default' ,
title ,
body ,
data ,
priority: 'high' ,
channelId: 'default' ,
};
await fetch ( 'https://exp.host/--/api/v2/push/send' , {
method: 'POST' ,
headers: {
Accept: 'application/json' ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ( message ),
});
};
import requests
import json
def send_push_notification ( expo_push_token , title , body , data = None ):
message = {
'to' : expo_push_token,
'sound' : 'default' ,
'title' : title,
'body' : body,
'data' : data or {},
'priority' : 'high' ,
'channelId' : 'default' ,
}
response = requests.post(
'https://exp.host/--/api/v2/push/send' ,
headers = {
'Accept' : 'application/json' ,
'Content-Type' : 'application/json' ,
},
data = json.dumps(message)
)
return response.json()
Apple Push Notification Service
Upload to Expo
Select your iOS app and upload the APNs key.
Enable push notifications capability
In your app.json: {
"expo" : {
"ios" : {
"entitlements" : {
"aps-environment" : "production"
}
}
}
}
Firebase Cloud Messaging
Create a Firebase project
Go to Firebase Console
Create a new project or select an existing one
Add an Android app with your package name
Download google-services.json
Download the google-services.json file and place it in your project root.
Upload FCM server key to Expo
Select your Android app and upload the FCM server key (found in Firebase Project Settings > Cloud Messaging).
Notification Channels (Android)
Android 8.0+ requires notification channels:
if ( Platform . OS === 'android' ) {
await Notifications . setNotificationChannelAsync ( 'messages' , {
name: 'Messages' ,
importance: Notifications . AndroidImportance . HIGH ,
vibrationPattern: [ 0 , 250 , 250 , 250 ],
sound: 'message-sound.wav' ,
lightColor: '#FF231F7C' ,
lockscreenVisibility: Notifications . AndroidNotificationVisibility . PUBLIC ,
bypassDnd: false ,
});
await Notifications . setNotificationChannelAsync ( 'alerts' , {
name: 'Alerts' ,
importance: Notifications . AndroidImportance . MAX ,
vibrationPattern: [ 0 , 500 , 500 , 500 ],
sound: 'alert-sound.wav' ,
});
}
Testing Notifications
Use the Expo Push Notification Tool to send test notifications:
Get your Expo push token from your app
Enter it in the tool
Compose and send a test notification
Test locally
import * as Notifications from 'expo-notifications' ;
// In your component
const sendTestNotification = async () => {
await Notifications . scheduleNotificationAsync ({
content: {
title: 'Test Notification' ,
body: 'This is a test' ,
data: { testData: 'test' },
},
trigger: null , // Send immediately
});
};
Troubleshooting
Notifications not received on iOS
Ensure you’re testing on a physical device (not simulator)
Check that APNs credentials are correctly configured
Verify push notifications capability is enabled
Check that your app is properly signed
Notifications not showing when app is foregrounded
Make sure you’ve set up the notification handler: Notifications . setNotificationHandler ({
handleNotification : async () => ({
shouldShowAlert: true ,
shouldPlaySound: true ,
shouldSetBadge: true ,
}),
});
Push token not generating
Verify you’re using a physical device
Check that all permissions are granted
For iOS: Ensure APNs is properly configured
For Android: Verify google-services.json is present
Ensure the sound file is in the correct format (WAV or MP3)
Add the sound file to your assets and reference it in app.json
For Android, set up the notification channel with the sound
Push notifications require physical devices for testing. The iOS simulator and some Android emulators don’t support push notifications.
Best Practices
Request permission contextually : Ask for notification permissions when the user takes an action that benefits from notifications
Use notification channels : Create separate channels for different types of notifications on Android
Include deep links : Add data to notifications to navigate users to relevant content
Handle notification lifecycle : Listen for notifications in all app states (foreground, background, quit)
Test thoroughly : Test on both iOS and Android physical devices
Store tokens securely : Send push tokens to your backend and associate them with user accounts
Handle token refresh : Listen for token updates and update your backend
Badge management : Clear badges when appropriate using Notifications.setBadgeCountAsync(0)