carlos Published June 30, 2022 · 24 min read

Adding notifications to your expo app

A decorative hero image for this page.

Push notifications are a fundamental part of any mobile app in order to comunicate with your users, so in today's post we are going to add push notifications support to your expo app.

expo

Using notifcations expo module we will add support for them in our proyect. Also we are going to implement the corresponding actions for user's interaction with them such as navigate to a screen in our app.

Adding expo notification module

As always first-time step is install the necessary packages.

yarn add expo-notifications

Getting an expo-push-token

First thing we have to code in order to have our notifications working is getting an expo notification token.

This token will be needed to identify our user and send them the corresponding notifications as it is unique by device.

Let's add the function that take care of this in our file App.js.

const registerForPushNotificationsAsync = async () => {
  let token;

  if (Constants.isDevice) {
    // we check if we have access to the notification permission
    const { status: existingStatus } = await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;


    if (existingStatus !== 'granted') {
      // if we don't have access to it, we ask for it
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }
    if (finalStatus !== 'granted') {
      // user does not allow us to access to the notifications
      alert('Failed to get push token for push notification!');
      return;
    }

    // obtain the expo token
    token = (await Notifications.getExpoPushTokenAsync()).data;

    // log the expo token in order to play with it
    console.log(token);
  } else {

    // notifications only work on physcal devices
    alert('Must use physical device for Push Notifications');
  }

  // some android configuration
  if (Platform.OS === 'android') {
    Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C',
    });
  }

  return token;
}

We check that we are using a physical device with the Constants module because notifications dont work on emulators, so in order to develope it you must use a physical phone with expo go.

if (Constants.isDevice)

If you don't know how to do it I recommend you the post about setting-up an expo proyect.

Once we've checked we are not using an emulator the very next step is knowing if the user has granted us the phone notification permission.

const { status: existingStatus } = await Notifications.getPermissionsAsync();

If we don't have it we request it to the user.

if (existingStatus !== 'granted') {
    // if we dontt have access to it, we ask for it
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
}
if (finalStatus !== 'granted') {
    // user doesnt allow us to access to the notifications
    alert('Failed to get push token for push notification!');
    return;
}

If the user does not give us the permission the game is over here.

In other case we can obtain our expo token.

token = (await Notifications.getExpoPushTokenAsync()).data;

Finally we add some necesary Android settings.

Building the notifcation listeners

The next step is setting up the functions that handle the notification when these arrive to the phone.

The first function is launched every time a notification arrives. The most common case will be save the notification in our redux state in order to show it in a inbox screen.

// This listener is fired whenever a notification is received while the app is foregrounded
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
    // save it to the store
    notificationCommonHandler(notification)
});

The second one is fired every time the user taps on the notification. In this case we are also saving the notification but it would be probably to add another action, like navigate to a screen within our app.

// This listener is fired whenever a user taps on or interacts with a notification 
// (works when app is foregrounded, backgrounded, or killed)
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
    notificationCommonHandler(response.notification);
    notificationNavigationHandler(response.notification.request.content);
});

The current state of the App.js should be looking like this:

import React, { useEffect, useRef } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import Navigator from './src/navigation';
import { store, persistor } from '@state/store';

const registerForPushNotificationsAsync = async () => {
  let token;

  if (Constants.isDevice) {
    // we check if we have access to the notification permission
    const { status: existingStatus } = await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;


    if (existingStatus !== 'granted') {
      // if we dontt have access to it, we ask for it
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }
    if (finalStatus !== 'granted') {
      // user doesnt allow us to access to the notifications
      alert('Failed to get push token for push notification!');
      return;
    }

    // obtain the expo token
    token = (await Notifications.getExpoPushTokenAsync()).data;

    // log the expo token in order to play with it
    console.log(token);
  } else {

    // notifications only work on physcal devices
    alert('Must use physical device for Push Notifications');
  }

  // some android configuration
  if (Platform.OS === 'android') {
    Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C',
    });
  }

  return token;
}

export default function App() {
  const notificationListener = useRef();
  const responseListener = useRef();

  useEffect( () => {
    // Register for push notification
    const token = registerForPushNotificationsAsync();

    // This listener is fired whenever a notification is received while the app is foregrounded
    notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
      notificationCommonHandler(notification);
    });

    // This listener is fired whenever a user taps on or interacts with a notification 
    // (works when app is foregrounded, backgrounded, or killed)
    responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
      notificationCommonHandler(response.notification);
      notificationNavigationHandler(response.notification.request.content);
    });

    // The listeners must be clear on app unmount
    return () => {
      Notifications.removeNotificationSubscription(notificationListener);
      Notifications.removeNotificationSubscription(responseListener);
    };
  }, []);

  const notificationCommonHandler = (notification) => {
    // save the notification to reac-redux store
    console.log('A notification has been received', notification)
  }


  const notificationNavigationHandler = ({ data }) => {
    // navigate to app screen
    console.log('A notification has been touched', data)
  }

  return (
    <Provider store={store}>
        <PersistGate persistor={persistor} loading={null}>
            <View style={styles.container}>
                <Navigator />
            </View>
        </PersistGate>
    </Provider>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
});

Remember that react useEffect hook is fired when the screen is mounted for the first time, in this case when te app is launched.

As alwaws if you have copied well and have followed all the steps you should be looking at your expo token once the project is started.

Playing around the Expo Push Notification Tool

We are going to use Expo Push Notification Tool in order to check our notifications are working properly.

Copy the expo token, write something in the title and body fields and fill with default the Android Channel ID. Once you press send notification you should be seeing it in your phone.

Expo notification tool interface.

Saving up the expo token on redux

As we dont want to register a token every time the app is launched we are going to save it in our redux state.

If you dont know how to set up or what is a redux state you can check the post about adding react-redux to your expo app.

We add a new action and it's corresponding type to the actions/users.js file.

export const SAVE_EXPO_TOKEN = 'SAVE_EXPO_TOKEN';
export const saveExpoToken = (expoToken) => ({ type: SAVE_EXPO_TOKEN, expoToken });

And we update our reductor in order to handle this new action.

import { combineReducers } from 'redux';

import { UPDATE_USERNAME, SAVE_EXPO_TOKEN } from '@actions/users';


const user  = (user = { username: '', expoToken: ''}, action) => {
    switch (action.type) {
        case UPDATE_USERNAME:
            return { ...user, username: action.username}
        case SAVE_EXPO_TOKEN:
            return { ...user, expoToken: action.expoToken}
        default:
            return user;
    }
}

export default combineReducers({ user });

Finally we have to check in the App.js function if we already have a token before register a new one.

As we dont have in the App component access to our redux state, remeber that we only have access to it within the provider component, we need to do some updates.

We create a new component called StatefullApp and we move the current logic and functions inside it.

import React, { useEffect, useRef } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import { Provider } from 'react-redux';
import Navigator from './src/navigation';
import { store, persistor } from './src/state/store';

const registerForPushNotificationsAsync = async () => {
    let token;

    if (Constants.isDevice) {
        // we check if we have access to the notification permission
        const { status: existingStatus } = await Notifications.getPermissionsAsync();
        let finalStatus = existingStatus;


        if (existingStatus !== 'granted') {
            // if we dontt have access to it, we ask for it
            const { status } = await Notifications.requestPermissionsAsync();
            finalStatus = status;
        }
        if (finalStatus !== 'granted') {
            // user doesnt allow us to access to the notifications
            alert('Failed to get push token for push notification!');
            return;
        }

        // obtain the expo token
        token = (await Notifications.getExpoPushTokenAsync()).data;

        // log the expo token in order to play with it
        console.log(token);
    } else {

        // notifications only work on physcal devices
        alert('Must use physical device for Push Notifications');
    }

    // some android configuration
    if (Platform.OS === 'android') {
        Notifications.setNotificationChannelAsync('default', {
            name: 'default',
            importance: Notifications.AndroidImportance.MAX,
            vibrationPattern: [0, 250, 250, 250],
            lightColor: '#FF231F7C',
        });
    }

    return token;
}


const StatefullApp = () => {
    const notificationListener = useRef();
    const responseListener = useRef();

    useEffect(() => {
        // Register for push notification
        const token = registerForPushNotificationsAsync();

        // This listener is fired whenever a notification is received while the app is foregrounded
        notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
            notificationCommonHandler(notification);
        });

        // This listener is fired whenever a user taps on or interacts with a notification 
        // (works when app is foregrounded, backgrounded, or killed)
        responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
            notificationCommonHandler(response.notification);
            notificationNavigationHandler(response.notification.request.content);
        });

        // The listeners must be clear on app unmount
        return () => {
            Notifications.removeNotificationSubscription(notificationListener);
            Notifications.removeNotificationSubscription(responseListener);
        };
    }, []);

    const notificationCommonHandler = (notification) => {
        // save the notification to reac-redux store
        console.log('A notification has been received', notification)
    }


    const notificationNavigationHandler = ({ data }) => {
        // navigate to app screen
        console.log('A notification has been touched', data)
    }

    return (
        <View style={styles.container}>
            <Navigator />
        </View>
    );
}

export default function App() {

    return (
      <Provider store={store}>
        <PersistGate persistor={persistor} loading={null}>
            <StatefullApp />
        </PersistGate>
      </Provider>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#fff',
    },
});

Now that we have access to our redux state inside the StatefullApp component.

const expoToken = useSelector( state => state.user.expoToken);

We check if we already have one before the registration call.

if ( expoToken === ''){
    const token = registerForPushNotificationsAsync();
}

In case we didn't have one we have to save it.

if ( expoToken === ''){
    const token = registerForPushNotificationsAsync();
    dispatch(saveExpoToken(token));
}

Summing app our Apps.js file should look like this:

import React, { useEffect, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { StyleSheet, Text, View } from 'react-native';
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import { Provider } from 'react-redux';
import Navigator from './src/navigation';
import { store, persistor } from '@state/store';
import { saveExpoToken } from './src/actions/users';

const registerForPushNotificationsAsync = async () => {
  let token;

  if (Constants.isDevice) {
    // we check if we have access to the notification permission
    const { status: existingStatus } = await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;


    if (existingStatus !== 'granted') {
      // if we dontt have access to it, we ask for it
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }
    if (finalStatus !== 'granted') {
      // user doesnt allow us to access to the notifications
      alert('Failed to get push token for push notification!');
      return;
    }

    // obtain the expo token
    token = (await Notifications.getExpoPushTokenAsync()).data;

    // log the expo token in order to play with it
    console.log(token);
  } else {

    // notifications only work on physcal devices
    alert('Must use physical device for Push Notifications');
  }

  // some android channel configuration
  if (Platform.OS === 'android') {
    Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C',
    });
  }

  return token;
}


const StatefullApp = () => {
  const dispatch = useDispatch();

  const expoToken = useSelector(state => state.user.expoToken);

  const notificationListener = useRef();
  const responseListener = useRef();

  useEffect(() => {
    // Register for push notification
    if (expoToken === '') {
      const token = registerForPushNotificationsAsync();
      dispatch(saveExpoToken(token));
    }

    // This listener is fired whenever a notification is received while the app is foregrounded
    notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
      notificationCommonHandler(notification);
    });

    // This listener is fired whenever a user taps on or interacts with a notification 
    // (works when app is foregrounded, backgrounded, or killed)
    responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
      notificationCommonHandler(response.notification);
      notificationNavigationHandler(response.notification.request.content);
    });

    // The listeners must be clear on app unmount
    return () => {
      Notifications.removeNotificationSubscription(notificationListener);
      Notifications.removeNotificationSubscription(responseListener);
    };
  }, []);

  const notificationCommonHandler = (notification) => {
    // save the notification to reac-redux store
    console.log('A notification has been received', notification)
  }


  const notificationNavigationHandler = ({ data }) => {
    // navigate to app screen
    console.log('A notification has been touched', data)
  }

  return(
    <View style={styles.container}>
      <Navigator />
    </View>
  )
}

export default function App() {

  return (
    <Provider store={store}>
        <PersistGate persistor={persistor} loading={null}>
            <StatefullApp />
        </PersistGate>
    </Provider>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
});

Good practices

App.js file is now a huge file and we want to mantain it as simple and legible as possible.

Even more the expo token register function does not ineract with tge screen state, so we are going to move it to a new directory of util functions.

.
├── src
│   ├── state
│   ├── actions
│   ├── utils
│   │   └── libraries.js
│   ├── navigation
│   ├── scenes
│   ├── components
│   └── atoms
└── App.js

Let's create a new utils directory within we add a new libraries.js file where we are going to put all functions that depends of external libraries.

Remember adding the import to App.js.

import { registerForPushNotificationsAsync } from '@utils/libraries';

If you don't know how to set up the alias you can visit our post about first steeps and good practices.

Adding navigation support

If you dont have already a navigator, set it up before doing this step.

In many cases we would like to navigate to a screen when user taps on a notification.

So we need to acces the navigate prop from the navigator object, but we can only access to this prop within the Navigation Container.

In this case we will solve the problem using react references, a reference is an identifier of a component that allows you using it's methods from external components.

If you want a more complete explanation you should visit the react documentation.

We create the reference that we are going to pass to our navigator in order to use it "from above".

const nav = useRef();

And we add it to our navigator.

return (
    <View style={styles.container}>
        <Navigator ref={nav} />
    </View>
)

Last thing is modify src/navigations/index.js to link the reference to our navigator.

import React, { forwardRef } from 'react';
import { NavigationContainer} from '@react-navigation/native';
import { MainStackNavigator } from './MainStackNavigator';


const RootNavigator = forwardRef((props, ref) => {

    return (
        <NavigationContainer ref={ref} {...props}>
            <MainStackNavigator />
        </NavigationContainer>
    )
})

export default RootNavigator;

Through forwardRef function from react we can pass to a child component a reference from its parent in order to be used.

So coming back to StatefullApp component lets define how the app should work when the users interacts with the notification using for this the notificationNavigationHandler defined at the begining.

For this let's supose our notification has a data object with the following structure.

{
    uuid: “”,
    navTo: “”,
    params: “”
}

If your are using an API is a common practice has serializer objects with an uuid associated, also we are going to pass a reference to know where to navigate within the app and some parameters.

As it could be the username if we want to go to some profile because of a following notification for example.

const notificationNavigationHandler = ({ data }) => {
    // navigate to app screen
    console.log('A notification has been touched', data)

    switch (data.navTo) {
      case 'details':
        nav.current.navigate('Details');
        return;
      default:
        nav.current.navigate('Home');
    }
}

In our case let's use the Details.js screen, that have being made in a previous post.

Modifying the icon and color of Android notification

Last thing of the post (I promise) is modify the color and icon of the notification to match our app icon. We can only do it for Android devices.

For this we need to modify the app.json file and include the following in a new key within the expo one.

"plugins": [
      [
        "expo-notifications",
        {
          "icon": "./assets/icon.png",
          "color": "#ffffff",
          "sounds": [
            "./local/path/to/mySound.wav",
            "./local/path/to/myOtherSound.wav"
          ],
          "mode": "production"
        }
      ]
    ]

And that's everything about client side notifications, hope it has been helpful and see you on the next one!

Ready to bring your vision to life?

Get in touch