Week 14: Push Notifications

A notification that arrives while the app is closed is the single most reliable way to bring a user back — and it's also one of the more involved features in this course, touching permissions, a token registration step, a real cloud messaging service, and different behavior depending on whether the app is open when it arrives.

Phase 7 of 8 Week 14 of 22 ~4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Set up expo-notifications and request notification permission correctly
  • Register for and retrieve a push token
  • Explain what FCM and APNs each do, and where expo-notifications sits relative to them
  • Handle a notification differently depending on whether it arrives in the foreground or is tapped from the background

1. Setting Up expo-notifications

terminal
npx expo install expo-notifications expo-device
notifications/setup.ts — how notifications behave in the foreground
import * as Notifications from 'expo-notifications';

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

This handler runs whenever a notification arrives while the app is open — without it, a foreground notification is received silently with no visible alert, which surprises most people building this for the first time.

2. Getting a Push Token

notifications/register.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';

export async function registerForPushNotifications() {
  if (!Device.isDevice) return null; // push tokens require a real device

  const { status: existing } = await Notifications.getPermissionsAsync();
  let finalStatus = existing;
  if (existing !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }
  if (finalStatus !== 'granted') return null;

  const token = await Notifications.getExpoPushTokenAsync({
    projectId: 'your-eas-project-id',
  });
  return token.data; // send this to your backend, keyed to the signed-in user
}

This token is what your backend sends notifications to — store it against the signed-in user's record the same way you'd store any other device metadata, and re-register (tokens can change) on every app launch.

3. Under the Hood: FCM & APNs

expo-notifications doesn't invent its own delivery network — it's a unified layer over each platform's real push service:

  • FCM (Firebase Cloud Messaging) — Android's delivery service. A message sent through FCM reaches the device even if the app isn't running.
  • APNs (Apple Push Notification service) — the iOS equivalent, requiring a signed push certificate or key registered with Apple.

Sending through Expo's push service (as the token above implies) means your backend talks to one simple API, and Expo's service handles routing the message to FCM or APNs correctly depending on the token's platform — you can also bypass Expo's service and talk to FCM/APNs directly for more control, at the cost of implementing both.

4. Handling Taps & Foreground Arrivals

a component listening for both cases
useEffect(() => {
  const receivedSub = Notifications.addNotificationReceivedListener((notification) => {
    console.log('Received in foreground:', notification.request.content.title);
  });

  const responseSub = Notifications.addNotificationResponseReceivedListener((response) => {
    const postId = response.notification.request.content.data.postId;
    navigation.navigate('PostDetail', { postId });
  });

  return () => {
    receivedSub.remove();
    responseSub.remove();
  };
}, [navigation]);

addNotificationReceivedListener fires when a notification arrives while the app is in the foreground (paired with the handler from earlier deciding whether to show an alert). addNotificationResponseReceivedListener fires when the user taps a notification — foreground, background, or from a fully closed app — and is where deep-link-style navigation, using data attached to the notification payload, belongs.

5. Hands-on Exercise

Hands-on

Build a working push notification flow

Request permission, register a token, send a real test push, and handle both delivery cases.

Requirements:

  1. A permission flow that checks existing status before requesting, and stores the result of registration (a token or a clear denied state).
  2. Send yourself a real test notification using Expo's push notification tool (expo.dev/notifications) with a postId in the data payload.
  3. Confirm the notification triggers addNotificationReceivedListener and shows an alert when sent while the app is open.
  4. Confirm tapping the notification (with the app backgrounded, and again with it fully closed) navigates to the correct PostDetail screen using the payload's postId.
  5. Both listeners cleaned up correctly on unmount.
Hint

Push notifications don't work in the iOS Simulator or most emulator configurations — this exercise genuinely needs a physical device with a development build installed (not Expo Go, for anything beyond the most basic test), so budget time for that setup if you haven't built one yet.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why does a notification arrive silently, with no visible alert, if setNotificationHandler is never called?

Without a handler telling expo-notifications what to do, a foreground-arriving notification has no instruction to show an alert, play a sound, or set a badge — the handler's returned object is what opts into each of those behaviors. This is the single most common "why isn't this working" moment when first setting up notifications.

Q2

What is a push token, and why does it need to be sent to your backend rather than kept only on the device?

It's the address your backend's server needs in order to send a notification to that specific device through Expo's push service (or FCM/APNs directly) — without your backend having it stored, there's no way to target a notification at that user's device at all.

Q3

What do FCM and APNs each actually deliver, and where does expo-notifications fit relative to them?

FCM (Android) and APNs (iOS) are the two platforms' real, underlying push delivery networks — the thing that actually gets a message to a device, even one whose app isn't running. expo-notifications (via Expo's push service) sits on top of both, giving your backend one unified API instead of implementing FCM and APNs integration separately.

Q4

Why does handling a notification tap need a different listener than handling a foreground arrival?

A tap can happen from three very different app states — foreground, backgrounded, or fully closed — and represents user intent to act on the notification (typically, navigate somewhere), while a foreground arrival is just informational and the app is already running normally. addNotificationResponseReceivedListener is specifically the one guaranteed to fire across all three tap scenarios, which a plain "received" listener is not designed to guarantee for a background or closed app.