Skip to main content

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

1

Install the package

npx expo install expo-notifications expo-device expo-constants
2

Configure your app.json

Add notification configuration to your app.json:
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"]
      }
    }
  }
}
3

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

Configure notification behavior

Set how notifications are displayed when the app is foregrounded:
App.tsx
import * as Notifications from 'expo-notifications';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

Listen for notifications

useEffect(() => {
  const subscription = Notifications.addNotificationReceivedListener(notification => {
    console.log('Notification received:', notification);
    // Update UI, show in-app notification, etc.
  });

  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),
  });
};

Platform-Specific Setup

Apple Push Notification Service

1

Create an APNs key

  1. Go to Apple Developer Portal
  2. Create a new key with Push Notifications enabled
  3. Download the .p8 file
2

Upload to Expo

eas credentials
Select your iOS app and upload the APNs key.
3

Enable push notifications capability

In your app.json:
{
  "expo": {
    "ios": {
      "entitlements": {
        "aps-environment": "production"
      }
    }
  }
}

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

Test with Expo push tool

Use the Expo Push Notification Tool to send test notifications:
  1. Get your Expo push token from your app
  2. Enter it in the tool
  3. 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

  • 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
Make sure you’ve set up the notification handler:
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});
  • 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)