DEV Community

Cover image for React Native Push Notifications
collegewap
collegewap

Posted on • Originally published at codingdeft.com

React Native Push Notifications

In this tutorial, you will learn:

  • How to display a local notification when the app is both in background and foreground.
  • How to send push notifications in React Native using Expo.
  • How to send push notifications from one phone to another.
  • To build a back-end to send push notifications to your users.

Local Notifications

You might have come across apps which will display notification locally, such as a reminder app.
These notifications are called local notifications since they are sent from within the app.

First, let's learn how to display a local notification in our app and
later we will make use of these local notifications to display push notifications.

Creating the Project

Create a new Expo project using the following command.
While creating the project, it would ask you to select a template. Choose "blank" template.

expo init react-native-push-notifications
Enter fullscreen mode Exit fullscreen mode

In order to show notifications, we need to install the package expo-notifications.
Also, in iOS we need explicit permission from the user to display notifications.
The package expo-permissions is used to handle permissions. So let's install both of them by running the following command:

expo install expo-notifications expo-permissions
Enter fullscreen mode Exit fullscreen mode

Now open app.json and add "useNextNotificationsApi": true for enabling notifications in Android:

{
  "expo": {
    "name": "react-native-push-notifications",
    "slug": "react-native-push-notifications",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": ["**/*"],
    "ios": {
      "supportsTablet": true
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#FFFFFF"
      },
      "useNextNotificationsApi": true
    },
    "web": {
      "favicon": "./assets/favicon.png"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Update the App.js with the following code:

import React, { useEffect } from "react"
import { StyleSheet, View, Button } from "react-native"
import * as Notifications from "expo-notifications"
import * as Permissions from "expo-permissions"

// Show notifications when the app is in the foreground
Notifications.setNotificationHandler({
  handleNotification: async () => {
    return {
      shouldShowAlert: true,
    }
  },
})

export default function App() {
  useEffect(() => {
    // Permission for iOS
    Permissions.getAsync(Permissions.NOTIFICATIONS)
      .then(statusObj => {
        // Check if we already have permission
        if (statusObj.status !== "granted") {
          // If permission is not there, ask for the same
          return Permissions.askAsync(Permissions.NOTIFICATIONS)
        }
        return statusObj
      })
      .then(statusObj => {
        // If permission is still not given throw error
        if (statusObj.status !== "granted") {
          throw new Error("Permission not granted")
        }
      })
      .catch(err => {
        return null
      })
  }, [])

  const triggerLocalNotificationHandler = () => {
    Notifications.scheduleNotificationAsync({
      content: {
        title: "Local Notification",
        body: "Hello this is a local notification!",
      },
      trigger: { seconds: 5 },
    })
  }

  return (
    <View style={styles.container}>
      <Button
        title="Trigger Local Notification"
        onPress={triggerLocalNotificationHandler}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
})
Enter fullscreen mode Exit fullscreen mode

Displaying Notifications when the App is in the Foreground

By default, notifications will be displayed only when the app is running in the background.
But there will be scenarios where you would want to show the notification when the app is running in the foreground,
like informing the user about the completion of a task or reminding them to do something.
To enable the notifications when the app is in the foreground,
we call setNotificationHandler function with a handleNotification callback and pass shouldShowAlert as true

Requesting Permission in iOS

In iOS, you need explicit permission from the user to show the notification.
For that, we call Permissions.getAsync(Permissions.NOTIFICATIONS) to check if we already have the permission.
If we do not have the permission, then we call Permissions.askAsync(Permissions.NOTIFICATIONS) to get permission from the user.
If the user does not provide the permission, then we throw an error.
However, we are not handling the error in the catch block in our code.
You may show an alert to the user telling them they need to provide the permission.

Scheduling the Notification

When the button is pressed, we call Notifications.scheduleNotificationAsync inside triggerLocalNotificationHandler function.
It takes an object as an argument with properties content and trigger.

  • content - We can pass the notification title and body inside the content. You can check out the other properties here.
  • trigger - Tells when to show the notification. You can specify an absolute time or a relative time. In our case, we are specifying a relative time of 5 seconds from now. If you want the notification to be shown immediately, you can pass the trigger as null.

To repeat the notification, you can use the repeat property as shown below:

Notifications.scheduleNotificationAsync({
  content: {
    title: 'Remember to drink water!,
  },
  trigger: {
    seconds: 60 * 20,
    repeats: true
  },
});
Enter fullscreen mode Exit fullscreen mode

You can read about other ways of using the trigger input here.

Now if you run the app in iOS, you will be asked for permission:

iOS Permission

If you press the button, should be able to see the notification after 5 seconds:

Local Notification

Handling Received Notifications

You can subscribe to the received notification by passing a callback to Notifications.addNotificationReceivedListener and
add a function to be called when the notification is clicked by passing it to Notifications.addNotificationResponseReceivedListener as shown below:

import React, { useEffect } from "react"
import { StyleSheet, View, Button } from "react-native"
import * as Notifications from "expo-notifications"
import * as Permissions from "expo-permissions"

// Show notifications when the app is in the foreground
Notifications.setNotificationHandler({
  handleNotification: async () => {
    return {
      shouldShowAlert: true,
    }
  },
})

export default function App() {
  useEffect(() => {
    // Permission for iOS
    Permissions.getAsync(Permissions.NOTIFICATIONS)
      .then(statusObj => {
        // Check if we already have permission
        if (statusObj.status !== "granted") {
          // If permission is not there, ask for the same
          return Permissions.askAsync(Permissions.NOTIFICATIONS)
        }
        return statusObj
      })
      .then(statusObj => {
        // If permission is still not given throw error
        if (statusObj.status !== "granted") {
          throw new Error("Permission not granted")
        }
      })
      .catch(err => {
        return null
      })
  }, [])

  useEffect(() => {
    const receivedSubscription = Notifications.addNotificationReceivedListener(
      notification => {
        console.log("Notification Received!")
        console.log(notification)
      }
    )

    const responseSubscription = Notifications.addNotificationResponseReceivedListener(
      response => {
        console.log("Notification Clicked!")
        console.log(response)
      }
    )
    return () => {
      receivedSubscription.remove()
      responseSubscription.remove()
    }
  }, [])

  const triggerLocalNotificationHandler = () => {
    Notifications.scheduleNotificationAsync({
      content: {
        title: "Local Notification",
        body: "Hello this is a local notification!",
      },
      trigger: { seconds: 5 },
    })
  }

  return (
    <View style={styles.container}>
      <Button
        title="Trigger Local Notification"
        onPress={triggerLocalNotificationHandler}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
})
Enter fullscreen mode Exit fullscreen mode

Push Notification

To send push notification to a device through Expo, we need to know the push token of that device.
We can fetch the push token by calling Notifications.getExpoPushTokenAsync function:

import React, { useEffect } from "react"
import { StyleSheet, View, Button } from "react-native"
import * as Notifications from "expo-notifications"
import * as Permissions from "expo-permissions"

// Show notifications when the app is in the foreground
Notifications.setNotificationHandler({
  handleNotification: async () => {
    return {
      shouldShowAlert: true,
    }
  },
})

export default function App() {
  useEffect(() => {
    // Permission for iOS
    Permissions.getAsync(Permissions.NOTIFICATIONS)
      .then(statusObj => {
        // Check if we already have permission
        if (statusObj.status !== "granted") {
          // If permission is not there, ask for the same
          return Permissions.askAsync(Permissions.NOTIFICATIONS)
        }
        return statusObj
      })
      .then(statusObj => {
        // If permission is still not given throw error
        if (statusObj.status !== "granted") {
          throw new Error("Permission not granted")
        }
      })
      .then(() => {
        return Notifications.getExpoPushTokenAsync()
      })
      .then(response => {
        const deviceToken = response.data
        console.log({ deviceToken })
      })
      .catch(err => {
        return null
      })
  }, [])

  useEffect(() => {
    const receivedSubscription = Notifications.addNotificationReceivedListener(
      notification => {
        console.log("Notification Received!")
        console.log(notification)
      }
    )

    const responseSubscription = Notifications.addNotificationResponseReceivedListener(
      response => {
        console.log("Notification Clicked!")
        console.log(response)
      }
    )
    return () => {
      receivedSubscription.remove()
      responseSubscription.remove()
    }
  }, [])

  const triggerLocalNotificationHandler = () => {
    Notifications.scheduleNotificationAsync({
      content: {
        title: "Local Notification",
        body: "Hello this is a local notification!",
      },
      trigger: { seconds: 5 },
    })
  }

  return (
    <View style={styles.container}>
      <Button
        title="Trigger Local Notification"
        onPress={triggerLocalNotificationHandler}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
})
Enter fullscreen mode Exit fullscreen mode

If you run the application now in the emulator, you will be able to see the token printed in the terminal.

Now you can send push notifications to the emulator using the expo notifications tool,
just by providing the title, message and Expo push token.

Notifications Tool Android

Push notifications are not supported in the iOS emulator. It needs to be tested in a real device.

Sending notification from one device to another

If you have to send notification from one device to another you can do so by posting it to the expo endpoint as shown in the code below:

import React, { useEffect, useState } from "react"
import { Button, StyleSheet, View, TextInput } from "react-native"
import * as Notifications from "expo-notifications"
import * as Permissions from "expo-permissions"

// Show notifications when the app is in the foreground
Notifications.setNotificationHandler({
  handleNotification: async () => {
    return {
      shouldShowAlert: true,
    }
  },
})

export default function App() {
  const [title, setTitle] = useState()
  const [body, setBody] = useState()
  const [token, setToken] = useState()

  useEffect(() => {
    // Permission for iOS
    Permissions.getAsync(Permissions.NOTIFICATIONS)
      .then(statusObj => {
        // Check if we already have permission
        if (statusObj.status !== "granted") {
          // If permission is not there, ask for the same
          return Permissions.askAsync(Permissions.NOTIFICATIONS)
        }
        return statusObj
      })
      .then(statusObj => {
        // If permission is still not given throw error
        if (statusObj.status !== "granted") {
          throw new Error("Permission not granted")
        }
      })
      .then(() => {
        return Notifications.getExpoPushTokenAsync()
      })
      .then(response => {
        const deviceToken = response.data
        console.log({ deviceToken })
      })
      .catch(err => {
        return null
      })
  }, [])

  useEffect(() => {
    const receivedSubscription = Notifications.addNotificationReceivedListener(
      notification => {
        console.log("Notification Received!")
        console.log(notification)
      }
    )

    const responseSubscription = Notifications.addNotificationResponseReceivedListener(
      response => {
        console.log("Notification Clicked!")
        console.log(response)
      }
    )
    return () => {
      receivedSubscription.remove()
      responseSubscription.remove()
    }
  }, [])

  const triggerLocalNotificationHandler = () => {
    Notifications.scheduleNotificationAsync({
      content: {
        title: "Local Notification",
        body: "Hello this is a local notification!",
      },
      trigger: { seconds: 5 },
    })
  }

  const triggerPushNotificationHandler = () => {
    fetch("https://exp.host/--/api/v2/push/send", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Accept-Encoding": "gzip,deflate",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        to: token,
        title,
        body,
      }),
    })
  }

  return (
    <View style={styles.container}>
      <Button
        title="Trigger Local Notification"
        onPress={triggerLocalNotificationHandler}
      />
      <TextInput
        style={styles.textInput}
        value={title}
        placeholder="Title"
        onChangeText={setTitle}
      />
      <TextInput
        style={styles.textInput}
        value={body}
        placeholder="Body"
        onChangeText={setBody}
      />
      <TextInput
        style={styles.textInput}
        value={token}
        placeholder="Token"
        onChangeText={setToken}
      />
      <Button
        title="Trigger Push Notification"
        onPress={triggerPushNotificationHandler}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  textInput: {
    borderBottomWidth: 1,
    padding: 5,
    margin: 15,
    width: "80%",
  },
})
Enter fullscreen mode Exit fullscreen mode

Here we have added 3 input boxes for entering notification title, notification body, and device token and bound them with local states.
When the button is pressed, the expo API is called with these details.

Building a custom notification tool

Since we require the device token of the user to send notifications, we need to store them somewhere so that we can use them in the future.
So, let's build a back-end, to where we can post the user token to save it to the database and a UI to retrieve the tokens and send the notifications.

Notification API back-end

Create a Node.js project using the following command:

npm init react-native-push-api

Enter fullscreen mode Exit fullscreen mode

Update the package.json as shown below:

{
  "name": "react-native-push-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "expo-server-sdk": "^3.6.0",
    "express": "^4.17.1",
    "mongoose": "^5.12.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

Run npm install to install the dependencies mentioned above.
If you see, we are using expo-server-sdk to help in sending the notification to the expo server.

We are using MongoDB for storing the tokens. You may use either a local instance or
connect to a cloud provider like MongoDB Atlas.

Now let's create a .env file and store the configurations there

MONGO_DB_CONNECTION_STRING = mongodb://127.0.0.1:27017/react_native_push
WHITELISTED_DOMAINS = http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

WHITELISTED_DOMAINS will be used for CORS whitelisting of the client.

Now create a folder named utils and create a file called connectdb.js which helps in connect to the database:

const mongoose = require("mongoose")
const url = process.env.MONGO_DB_CONNECTION_STRING
const connect = mongoose.connect(url, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useCreateIndex: true,
})
connect
  .then(db => {
    console.log("connected to db")
  })
  .catch(err => {
    console.log(err)
  })
Enter fullscreen mode Exit fullscreen mode

Create another file named expo.js with the following code.
The code is taken as-is from the expo-server-sdk GitHub page.

const { Expo } = require("expo-server-sdk")
module.exports = (pushTokens, title, body, data) => {
  // Create a new Expo SDK client
  // optionally providing an access token if you have enabled push security
  let expo = new Expo({ accessToken: process.env.EXPO_ACCESS_TOKEN })

  // Create the messages that you want to send to clients
  let messages = []
  for (let pushToken of pushTokens) {
    // Each push token looks like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]

    // Check that all your push tokens appear to be valid Expo push tokens
    if (!Expo.isExpoPushToken(pushToken)) {
      console.error(`Push token ${pushToken} is not a valid Expo push token`)
      continue
    }

    // Construct a message (see https://docs.expo.io/push-notifications/sending-notifications/)
    messages.push({
      to: pushToken,
      sound: "default",
      title,
      body,
      data,
    })
  }

  // The Expo push notification service accepts batches of notifications so
  // that you don't need to send 1000 requests to send 1000 notifications. We
  // recommend you batch your notifications to reduce the number of requests
  // and to compress them (notifications with similar content will get
  // compressed).
  let chunks = expo.chunkPushNotifications(messages)
  let tickets = []
  ;(async () => {
    // Send the chunks to the Expo push notification service. There are
    // different strategies you could use. A simple one is to send one chunk at a
    // time, which nicely spreads the load out over time:
    for (let chunk of chunks) {
      try {
        let ticketChunk = await expo.sendPushNotificationsAsync(chunk)
        console.log(ticketChunk)
        tickets.push(...ticketChunk)
        // NOTE: If a ticket contains an error code in ticket.details.error, you
        // must handle it appropriately. The error codes are listed in the Expo
        // documentation:
        // https://docs.expo.io/push-notifications/sending-notifications/#individual-errors
      } catch (error) {
        console.error(error)
      }
    }
  })()

  // Later, after the Expo push notification service has delivered the
  // notifications to Apple or Google (usually quickly, but allow the the service
  // up to 30 minutes when under load), a "receipt" for each notification is
  // created. The receipts will be available for at least a day; stale receipts
  // are deleted.
  //
  // The ID of each receipt is sent back in the response "ticket" for each
  // notification. In summary, sending a notification produces a ticket, which
  // contains a receipt ID you later use to get the receipt.
  //
  // The receipts may contain error codes to which you must respond. In
  // particular, Apple or Google may block apps that continue to send
  // notifications to devices that have blocked notifications or have uninstalled
  // your app. Expo does not control this policy and sends back the feedback from
  // Apple and Google so you can handle it appropriately.
  let receiptIds = []
  for (let ticket of tickets) {
    // NOTE: Not all tickets have IDs; for example, tickets for notifications
    // that could not be enqueued will have error information and no receipt ID.
    if (ticket.id) {
      receiptIds.push(ticket.id)
    }
  }

  let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds)
  ;(async () => {
    // Like sending notifications, there are different strategies you could use
    // to retrieve batches of receipts from the Expo service.
    for (let chunk of receiptIdChunks) {
      try {
        let receipts = await expo.getPushNotificationReceiptsAsync(chunk)
        console.log(receipts)

        // The receipts specify whether Apple or Google successfully received the
        // notification and information about an error, if one occurred.
        for (let receiptId in receipts) {
          let { status, message, details } = receipts[receiptId]
          if (status === "ok") {
            continue
          } else if (status === "error") {
            console.error(
              `There was an error sending a notification: ${message}`
            )
            if (details && details.error) {
              // The error codes are listed in the Expo documentation:
              // https://docs.expo.io/push-notifications/sending-notifications/#individual-errors
              // You must handle the errors appropriately.
              console.error(`The error code is ${details.error}`)
            }
          }
        }
      } catch (error) {
        console.error(error)
      }
    }
  })()
}
Enter fullscreen mode Exit fullscreen mode

Now create a folder named models and create a file called token.js inside it:

const mongoose = require("mongoose")
const Schema = mongoose.Schema
const Token = new Schema({
  tokenValue: {
    type: String,
    default: "",
  },
})

module.exports = mongoose.model("Token", Token)
Enter fullscreen mode Exit fullscreen mode

Here we are creating Token model to store the token value in the database.

Finally, create the index.js file and update it with the following code:

const express = require("express")
const cors = require("cors")
const bodyParser = require("body-parser")
const sendPushNotification = require("./utils/expo")

if (process.env.NODE_ENV !== "production") {
  // Load environment variables from .env file in non prod environments
  require("dotenv").config()
}

require("./utils/connectdb")

const Token = require("./models/token")

const app = express()
app.use(bodyParser.json())

// Add the client URL to the CORS policy
const whitelist = process.env.WHITELISTED_DOMAINS
  ? process.env.WHITELISTED_DOMAINS.split(",")
  : []
const corsOptions = {
  origin: function (origin, callback) {
    if (!origin || whitelist.indexOf(origin) !== -1) {
      callback(null, true)
    } else {
      callback(new Error("Not allowed by CORS"))
    }
  },
  credentials: true,
}

app.use(cors(corsOptions))

app.get("/", function (req, res) {
  res.send({ status: "success" })
})

app.post("/send_notification", function (req, res) {
  const { title, body, data, to } = req.body
  if (to === "all") {
    Token.find({}, (err, allTokens) => {
      if (err) {
        res.statusCode = 500
        res.send(err)
      }

      const tokens = allTokens.map(token => {
        return token.tokenValue
      })

      sendPushNotification(tokens, title, body, data)
      res.send({ status: "success" })
    })
  } else {
    sendPushNotification([to], title, body, data)
    res.send({ status: "success" })
  }
})
app.post("/save_token", function (req, res) {
  const token = req.body.token

  if (token) {
    Token.find({ tokenValue: token }, (err, existingToken) => {
      if (err) {
        res.statusCode = 500
        res.send(err)
      }
      if (!err && existingToken.length === 0) {
        const newToken = new Token({ tokenValue: req.body.token })

        newToken.save(function (err, savedToken) {
          if (err) {
            res.statusCode = 500
            res.send(err)
          }

          res.send({ status: "success" })
        })
      } else {
        res.send({ status: "success" })
      }
    })
  } else {
    res.statusCode = 400
    res.send({ message: "token not passed!" })
  }
})

app.get("/all_tokens", function (req, res) {
  Token.find({}, (err, allTokens) => {
    if (err) {
      res.statusCode = 500
      res.send(err)
    }
    res.send(
      allTokens.map(token => {
        // remove unnecessary fields
        return { value: token.tokenValue }
      })
    )
  })
})

// Start the server in port 8081
const server = app.listen(process.env.PORT || 8081, function () {
  const port = server.address().port
  console.log("App started at port:", port)
})
Enter fullscreen mode Exit fullscreen mode

Here we are having 3 routes:

  1. To send a notification to all the devices or a single device
  2. To save the device token to the database
  3. To fetch all the tokens from the database.

You may test it using postman. I have deployed it to Heroku and will be using that endpoint when we build the UI.

Building the notification console UI

Let's now go ahead and create a React Project to have a UI to send notifications using the API we have built.

Run the following command to create a new React project:

npx create-react-app push-api-client
Enter fullscreen mode Exit fullscreen mode

Install BlueprintJS to style the page:

yarn add @blueprintjs/core
Enter fullscreen mode Exit fullscreen mode

Now import BlueprintJS css in index.css

@import "~normalize.css";
@import "~@blueprintjs/core/lib/css/blueprint.css";
@import "~@blueprintjs/icons/lib/css/blueprint-icons.css";

body {
  margin: 0 auto;
  max-width: 400px;
}
Enter fullscreen mode Exit fullscreen mode

Now update the App.js with the following code:

import {
  Card,
  Button,
  FormGroup,
  InputGroup,
  H2,
  TextArea,
  Intent,
  HTMLSelect,
  Toaster,
  Position,
} from "@blueprintjs/core"
import React, { useEffect, useRef, useState } from "react"
const allOption = [{ value: "all", label: "All" }]

// Replace it with your own endpoint
const API_ENDPOINT = "https://react-native-push-api.herokuapp.com/"

function App() {
  const [title, setTitle] = useState("")
  const [body, setBody] = useState("")
  const [data, setData] = useState()
  const [recipients, setRecipients] = useState(allOption)
  const [to, setTo] = useState("all")
  const [isSubmitting, setIsSubmitting] = useState(false)
  const toastRef = useRef()

  useEffect(() => {
    fetch(API_ENDPOINT + "all_tokens").then(async response => {
      if (response.ok) {
        const tokens = await response.json()
        setRecipients(allOption.concat(tokens))
      }
    })
  }, [])

  const formSubmitHandler = e => {
    let parsedData = {}
    try {
      parsedData = data ? JSON.parse(data) : {}
    } catch (err) {
      console.log(err)
    }

    e.preventDefault()

    setIsSubmitting(true)
    fetch(API_ENDPOINT + "send_notification", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        to,
        title,
        body,
        data: parsedData,
      }),
    })
      .then(async response => {
        setIsSubmitting(false)

        if (response.ok) {
          toastRef.current.show({
            icon: "tick",
            intent: Intent.SUCCESS,
            message: "Notification sent successfully.",
          })
        } else {
          toastRef.current.show({
            icon: "warning-sign",
            intent: Intent.DANGER,
            message: "Something went wrong.",
          })
        }
      })
      .catch(err => {
        setIsSubmitting(false)
        toastRef.current.show({
          icon: "warning-sign",
          intent: Intent.DANGER,
          message: "Something went wrong.",
        })
      })
  }
  return (
    <Card elevation="1">
      <Toaster
        ref={toastRef}
        autoFocus={false}
        canEscapeKeyClear={true}
        position={Position.TOP}
        usePortal={true}
      />
      <H2>Send Push Notification</H2>
      <form className="notification-form" onSubmit={formSubmitHandler}>
        <FormGroup label="Notification Title" labelFor="title">
          <InputGroup
            id="title"
            placeholder="Notification Title"
            type="text"
            value={title}
            onChange={e => setTitle(e.target.value)}
          />
        </FormGroup>
        <FormGroup label="Notification Body" labelFor="body">
          <InputGroup
            id="body"
            placeholder="Notification Body"
            type="text"
            value={body}
            onChange={e => setBody(e.target.value)}
          />
        </FormGroup>
        <FormGroup label="Additional Data" labelFor="data">
          <TextArea
            growVertically={true}
            large={true}
            placeholder="Additional data in JSON"
            id="data"
            value={data}
            onChange={e => setData(e.target.value)}
            className="bp3-fill"
          />
        </FormGroup>
        <FormGroup label="Send To" labelFor="data">
          <HTMLSelect
            fill
            options={recipients}
            value={to}
            onChange={e => setTo(e.target.value)}
          />
        </FormGroup>
        <Button
          intent="primary"
          fill
          type="submit"
          text={isSubmitting ? "Sending" : "Send"}
        />
      </form>
    </Card>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Now if you run the application, you should see a page as shown below:

Notifications Console

Before testing the console, let's add code to send the device token from the App to the backend API.
In the Expo project, update the App.js as shown below:

import React, { useEffect, useState } from "react"
import { Button, StyleSheet, View, TextInput } from "react-native"
import * as Notifications from "expo-notifications"
import * as Permissions from "expo-permissions"

// Show notifications when the app is in the foreground
Notifications.setNotificationHandler({
  handleNotification: async () => {
    return {
      shouldShowAlert: true,
    }
  },
})

export default function App() {
  const [title, setTitle] = useState()
  const [body, setBody] = useState()
  const [token, setToken] = useState()

  useEffect(() => {
    // Permission for iOS
    Permissions.getAsync(Permissions.NOTIFICATIONS)
      .then(statusObj => {
        // Check if we already have permission
        if (statusObj.status !== "granted") {
          // If permission is not there, ask for the same
          return Permissions.askAsync(Permissions.NOTIFICATIONS)
        }
        return statusObj
      })
      .then(statusObj => {
        // If permission is still not given throw error
        if (statusObj.status !== "granted") {
          throw new Error("Permission not granted")
        }
      })
      .then(() => {
        return Notifications.getExpoPushTokenAsync()
      })
      .then(response => {
        const deviceToken = response.data
        fetch("https://react-native-push-api.herokuapp.com/save_token", {
          method: "POST",
          headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            token: deviceToken,
          }),
        })
          .then(() => {
            console.log("Token saved!")
          })
          .catch(err => {
            console.log(err)
          })
      })
      .catch(err => {
        return null
      })
  }, [])

  useEffect(() => {
    const receivedSubscription = Notifications.addNotificationReceivedListener(
      notification => {
        console.log("Notification Received!")
        console.log(notification)
      }
    )

    const responseSubscription = Notifications.addNotificationResponseReceivedListener(
      response => {
        console.log("Notification Clicked!")
        console.log(response)
      }
    )
    return () => {
      receivedSubscription.remove()
      responseSubscription.remove()
    }
  }, [])

  const triggerLocalNotificationHandler = () => {
    Notifications.scheduleNotificationAsync({
      content: {
        title: "Local Notification",
        body: "Hello this is a local notification!",
      },
      trigger: { seconds: 5 },
    })
  }

  const triggerPushNotificationHandler = () => {
    fetch("https://exp.host/--/api/v2/push/send", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Accept-Encoding": "gzip,deflate",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        to: token,
        title,
        body,
      }),
    })
  }

  return (
    <View style={styles.container}>
      <Button
        title="Trigger Local Notification"
        onPress={triggerLocalNotificationHandler}
      />
      <TextInput
        style={styles.textInput}
        value={title}
        placeholder="Title"
        onChangeText={setTitle}
      />
      <TextInput
        style={styles.textInput}
        value={body}
        placeholder="Body"
        onChangeText={setBody}
      />
      <TextInput
        style={styles.textInput}
        value={token}
        placeholder="Token"
        onChangeText={setToken}
      />
      <Button
        title="Trigger Push Notification"
        onPress={triggerPushNotificationHandler}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  textInput: {
    borderBottomWidth: 1,
    padding: 5,
    margin: 15,
    width: "80%",
  },
})
Enter fullscreen mode Exit fullscreen mode

Now if you run the App on few devices, you will see the tokens being saved to the database and displayed in the dropdown:

Notification Console Tokens

Now if you test the console and send the notification, you will see the devices getting the notification:

Android and iOS from Console

We are not having any authentication in place for the API endpoint.
When you implement it in production, you should have authentication of some kind so that the endpoint is not spammed or misused.

You could use additional security by enabling Access Tokens
so that even if the device token is stolen, it wouldn't be possible to send notifications without the access token.

Demo and Source code

You can see a demo of the Console here, the source code of the UI here, and the back-end API here.

App demo

You can see the app demo here.

Discussion (0)