React Native Push Notifications
Tags: reactnative, react, node
Published: 2021-04-16 03:21:11


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.

```bash
expo init react-native-push-notifications
```

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:

```bash
expo install expo-notifications expo-permissions
```

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

```json
{
  "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"
    }
  }
}
```

Update the `App.js` with the following code:

```jsx
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",
  },
})
```

### 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](https://www.codingdeft.com/posts/react-native-alerts/) 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](https://docs.expo.io/versions/latest/sdk/notifications/#notificationcontentinput).
- `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:

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

You can read about other ways of using the trigger input [here](https://docs.expo.io/versions/latest/sdk/notifications/#notificationtriggerinput).

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

![iOS Permission](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjqlrdqa0nr5kdcwd9je.png)

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

![Local Notification](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g9e6iprnwl4l9o06qzmp.png)

## 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:

```jsx
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",
  },
})
```

# 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:

```jsx
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",
  },
})
```

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](https://expo.io/notifications),
just by providing the title, message and Expo push token.

![Notifications Tool Android](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6h8dxhn1su4tizhw7whn.png)

> 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:

```jsx
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%",
  },
})
```

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:

```bash
npm init react-native-push-api

```

Update the `package.json` as shown below:

```json
{
  "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"
  }
}
```

Run `npm install` to install the dependencies mentioned above.
If you see, we are using [expo-server-sdk](https://www.npmjs.com/package/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](https://docs.mongodb.com/manual/installation/) or
connect to a cloud provider like [MongoDB Atlas](https://www.mongodb.com/cloud/atlas/).

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

```.env
MONGO_DB_CONNECTION_STRING = mongodb://127.0.0.1:27017/react_native_push
WHITELISTED_DOMAINS = http://localhost:3000
```

`WHITELISTED_DOMAINS` will be used for [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/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:

```js
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)
  })
```

Create another file named `expo.js` with the following code.
The code is taken as-is from the [expo-server-sdk GitHub page](https://github.com/expo/expo-server-sdk-node#usage).

```js
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)
      }
    }
  })()
}
```

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

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

module.exports = mongoose.model("Token", Token)
```

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:

```js
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)
})
```

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:

```bash
npx create-react-app push-api-client
```

Install [BlueprintJS](https://blueprintjs.com/docs/#core) to style the page:

```bash
yarn add @blueprintjs/core
```

Now import BlueprintJS css in `index.css`

```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;
}
```

Now update the `App.js` with the following code:

```jsx
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
```

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

![Notifications Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/818b9anx9z4ik7st6m5u.png)

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:

```jsx
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%",
  },
})
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j4idz53irjku46z9usx1.png)

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

![Android and iOS from Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/80ddxny5oyehuizzsio2.png)

> 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](https://expo.io/settings/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](https://push-api-client.vercel.app/), the source code of the UI [here](https://github.com/collegewap/push-api-client), and the back-end API [here](https://github.com/collegewap/react-native-push-api).

## App demo

You can see the app demo [here](https://snack.expo.io/@collegewap/push-notification).