Skip to content
react-native

React Native Push Notifications

Apr 14, 2021Abhishek EH21 Min Read
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.

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

1expo install expo-notifications expo-permissions

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

app.json
1{
2 "expo": {
3 "name": "react-native-push-notifications",
4 "slug": "react-native-push-notifications",
5 "version": "1.0.0",
6 "orientation": "portrait",
7 "icon": "./assets/icon.png",
8 "splash": {
9 "image": "./assets/splash.png",
10 "resizeMode": "contain",
11 "backgroundColor": "#ffffff"
12 },
13 "updates": {
14 "fallbackToCacheTimeout": 0
15 },
16 "assetBundlePatterns": ["**/*"],
17 "ios": {
18 "supportsTablet": true
19 },
20 "android": {
21 "adaptiveIcon": {
22 "foregroundImage": "./assets/adaptive-icon.png",
23 "backgroundColor": "#FFFFFF"
24 },
25 "useNextNotificationsApi": true
26 },
27 "web": {
28 "favicon": "./assets/favicon.png"
29 }
30 }
31}

Update the App.js with the following code:

App.js
1import React, { useEffect } from "react"
2import { StyleSheet, View, Button } from "react-native"
3import * as Notifications from "expo-notifications"
4import * as Permissions from "expo-permissions"
5
6// Show notifications when the app is in the foreground
7Notifications.setNotificationHandler({
8 handleNotification: async () => {
9 return {
10 shouldShowAlert: true,
11 }
12 },
13})
14
15export default function App() {
16 useEffect(() => {
17 // Permission for iOS
18 Permissions.getAsync(Permissions.NOTIFICATIONS)
19 .then(statusObj => {
20 // Check if we already have permission
21 if (statusObj.status !== "granted") {
22 // If permission is not there, ask for the same
23 return Permissions.askAsync(Permissions.NOTIFICATIONS)
24 }
25 return statusObj
26 })
27 .then(statusObj => {
28 // If permission is still not given throw error
29 if (statusObj.status !== "granted") {
30 throw new Error("Permission not granted")
31 }
32 })
33 .catch(err => {
34 return null
35 })
36 }, [])
37
38 const triggerLocalNotificationHandler = () => {
39 Notifications.scheduleNotificationAsync({
40 content: {
41 title: "Local Notification",
42 body: "Hello this is a local notification!",
43 },
44 trigger: { seconds: 5 },
45 })
46 }
47
48 return (
49 <View style={styles.container}>
50 <Button
51 title="Trigger Local Notification"
52 onPress={triggerLocalNotificationHandler}
53 />
54 </View>
55 )
56}
57
58const styles = StyleSheet.create({
59 container: {
60 flex: 1,
61 backgroundColor: "#fff",
62 alignItems: "center",
63 justifyContent: "center",
64 },
65})

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:

1Notifications.scheduleNotificationAsync({
2 content: {
3 title: 'Remember to drink water!,
4 },
5 trigger: {
6 seconds: 60 * 20,
7 repeats: true
8 },
9});

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:

1import React, { useEffect } from "react"
2import { StyleSheet, View, Button } from "react-native"
3import * as Notifications from "expo-notifications"
4import * as Permissions from "expo-permissions"
5
6// Show notifications when the app is in the foreground
7Notifications.setNotificationHandler({
8 handleNotification: async () => {
9 return {
10 shouldShowAlert: true,
11 }
12 },
13})
14
15export default function App() {
16 useEffect(() => {
17 // Permission for iOS
18 Permissions.getAsync(Permissions.NOTIFICATIONS)
19 .then(statusObj => {
20 // Check if we already have permission
21 if (statusObj.status !== "granted") {
22 // If permission is not there, ask for the same
23 return Permissions.askAsync(Permissions.NOTIFICATIONS)
24 }
25 return statusObj
26 })
27 .then(statusObj => {
28 // If permission is still not given throw error
29 if (statusObj.status !== "granted") {
30 throw new Error("Permission not granted")
31 }
32 })
33 .catch(err => {
34 return null
35 })
36 }, [])
37
38 useEffect(() => {
39 const receivedSubscription = Notifications.addNotificationReceivedListener(
40 notification => {
41 console.log("Notification Received!")
42 console.log(notification)
43 }
44 )
45
46 const responseSubscription =
47 Notifications.addNotificationResponseReceivedListener(response => {
48 console.log("Notification Clicked!")
49 console.log(response)
50 })
51 return () => {
52 receivedSubscription.remove()
53 responseSubscription.remove()
54 }
55 }, [])
56
57 const triggerLocalNotificationHandler = () => {
58 Notifications.scheduleNotificationAsync({
59 content: {
60 title: "Local Notification",
61 body: "Hello this is a local notification!",
62 },
63 trigger: { seconds: 5 },
64 })
65 }
66
67 return (
68 <View style={styles.container}>
69 <Button
70 title="Trigger Local Notification"
71 onPress={triggerLocalNotificationHandler}
72 />
73 </View>
74 )
75}
76
77const styles = StyleSheet.create({
78 container: {
79 flex: 1,
80 backgroundColor: "#fff",
81 alignItems: "center",
82 justifyContent: "center",
83 },
84})

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:

App.js
1import React, { useEffect } from "react"
2import { StyleSheet, View, Button } from "react-native"
3import * as Notifications from "expo-notifications"
4import * as Permissions from "expo-permissions"
5
6// Show notifications when the app is in the foreground
7Notifications.setNotificationHandler({
8 handleNotification: async () => {
9 return {
10 shouldShowAlert: true,
11 }
12 },
13})
14
15export default function App() {
16 useEffect(() => {
17 // Permission for iOS
18 Permissions.getAsync(Permissions.NOTIFICATIONS)
19 .then(statusObj => {
20 // Check if we already have permission
21 if (statusObj.status !== "granted") {
22 // If permission is not there, ask for the same
23 return Permissions.askAsync(Permissions.NOTIFICATIONS)
24 }
25 return statusObj
26 })
27 .then(statusObj => {
28 // If permission is still not given throw error
29 if (statusObj.status !== "granted") {
30 throw new Error("Permission not granted")
31 }
32 })
33 .then(() => {
34 return Notifications.getExpoPushTokenAsync()
35 })
36 .then(response => {
37 const deviceToken = response.data
38 console.log({ deviceToken })
39 })
40 .catch(err => {
41 return null
42 })
43 }, [])
44
45 useEffect(() => {
46 const receivedSubscription = Notifications.addNotificationReceivedListener(
47 notification => {
48 console.log("Notification Received!")
49 console.log(notification)
50 }
51 )
52
53 const responseSubscription =
54 Notifications.addNotificationResponseReceivedListener(response => {
55 console.log("Notification Clicked!")
56 console.log(response)
57 })
58 return () => {
59 receivedSubscription.remove()
60 responseSubscription.remove()
61 }
62 }, [])
63
64 const triggerLocalNotificationHandler = () => {
65 Notifications.scheduleNotificationAsync({
66 content: {
67 title: "Local Notification",
68 body: "Hello this is a local notification!",
69 },
70 trigger: { seconds: 5 },
71 })
72 }
73
74 return (
75 <View style={styles.container}>
76 <Button
77 title="Trigger Local Notification"
78 onPress={triggerLocalNotificationHandler}
79 />
80 </View>
81 )
82}
83
84const styles = StyleSheet.create({
85 container: {
86 flex: 1,
87 backgroundColor: "#fff",
88 alignItems: "center",
89 justifyContent: "center",
90 },
91})

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:

App.js
1import React, { useEffect, useState } from "react"
2import { Button, StyleSheet, View, TextInput } from "react-native"
3import * as Notifications from "expo-notifications"
4import * as Permissions from "expo-permissions"
5
6// Show notifications when the app is in the foreground
7Notifications.setNotificationHandler({
8 handleNotification: async () => {
9 return {
10 shouldShowAlert: true,
11 }
12 },
13})
14
15export default function App() {
16 const [title, setTitle] = useState()
17 const [body, setBody] = useState()
18 const [token, setToken] = useState()
19
20 useEffect(() => {
21 // Permission for iOS
22 Permissions.getAsync(Permissions.NOTIFICATIONS)
23 .then(statusObj => {
24 // Check if we already have permission
25 if (statusObj.status !== "granted") {
26 // If permission is not there, ask for the same
27 return Permissions.askAsync(Permissions.NOTIFICATIONS)
28 }
29 return statusObj
30 })
31 .then(statusObj => {
32 // If permission is still not given throw error
33 if (statusObj.status !== "granted") {
34 throw new Error("Permission not granted")
35 }
36 })
37 .then(() => {
38 return Notifications.getExpoPushTokenAsync()
39 })
40 .then(response => {
41 const deviceToken = response.data
42 console.log({ deviceToken })
43 })
44 .catch(err => {
45 return null
46 })
47 }, [])
48
49 useEffect(() => {
50 const receivedSubscription = Notifications.addNotificationReceivedListener(
51 notification => {
52 console.log("Notification Received!")
53 console.log(notification)
54 }
55 )
56
57 const responseSubscription =
58 Notifications.addNotificationResponseReceivedListener(response => {
59 console.log("Notification Clicked!")
60 console.log(response)
61 })
62 return () => {
63 receivedSubscription.remove()
64 responseSubscription.remove()
65 }
66 }, [])
67
68 const triggerLocalNotificationHandler = () => {
69 Notifications.scheduleNotificationAsync({
70 content: {
71 title: "Local Notification",
72 body: "Hello this is a local notification!",
73 },
74 trigger: { seconds: 5 },
75 })
76 }
77
78 const triggerPushNotificationHandler = () => {
79 fetch("https://exp.host/--/api/v2/push/send", {
80 method: "POST",
81 headers: {
82 Accept: "application/json",
83 "Accept-Encoding": "gzip,deflate",
84 "Content-Type": "application/json",
85 },
86 body: JSON.stringify({
87 to: token,
88 title,
89 body,
90 }),
91 })
92 }
93
94 return (
95 <View style={styles.container}>
96 <Button
97 title="Trigger Local Notification"
98 onPress={triggerLocalNotificationHandler}
99 />
100 <TextInput
101 style={styles.textInput}
102 value={title}
103 placeholder="Title"
104 onChangeText={setTitle}
105 />
106 <TextInput
107 style={styles.textInput}
108 value={body}
109 placeholder="Body"
110 onChangeText={setBody}
111 />
112 <TextInput
113 style={styles.textInput}
114 value={token}
115 placeholder="Token"
116 onChangeText={setToken}
117 />
118 <Button
119 title="Trigger Push Notification"
120 onPress={triggerPushNotificationHandler}
121 />
122 </View>
123 )
124}
125
126const styles = StyleSheet.create({
127 container: {
128 flex: 1,
129 backgroundColor: "#fff",
130 alignItems: "center",
131 justifyContent: "center",
132 },
133 textInput: {
134 borderBottomWidth: 1,
135 padding: 5,
136 margin: 15,
137 width: "80%",
138 },
139})

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:

1npm init react-native-push-api

Update the package.json as shown below:

package.json
1{
2 "name": "react-native-push-api",
3 "version": "1.0.0",
4 "description": "",
5 "main": "index.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1",
8 "start": "node index.js"
9 },
10 "keywords": [],
11 "author": "",
12 "license": "ISC",
13 "dependencies": {
14 "body-parser": "^1.19.0",
15 "cors": "^2.8.5",
16 "dotenv": "^8.2.0",
17 "expo-server-sdk": "^3.6.0",
18 "express": "^4.17.1",
19 "mongoose": "^5.12.3"
20 }
21}

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

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

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:

connectdb.js
1const mongoose = require("mongoose")
2const url = process.env.MONGO_DB_CONNECTION_STRING
3const connect = mongoose.connect(url, {
4 useNewUrlParser: true,
5 useUnifiedTopology: true,
6 useCreateIndex: true,
7})
8connect
9 .then(db => {
10 console.log("connected to db")
11 })
12 .catch(err => {
13 console.log(err)
14 })

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

1const { Expo } = require("expo-server-sdk")
2module.exports = (pushTokens, title, body, data) => {
3 // Create a new Expo SDK client
4 // optionally providing an access token if you have enabled push security
5 let expo = new Expo({ accessToken: process.env.EXPO_ACCESS_TOKEN })
6
7 // Create the messages that you want to send to clients
8 let messages = []
9 for (let pushToken of pushTokens) {
10 // Each push token looks like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]
11
12 // Check that all your push tokens appear to be valid Expo push tokens
13 if (!Expo.isExpoPushToken(pushToken)) {
14 console.error(`Push token ${pushToken} is not a valid Expo push token`)
15 continue
16 }
17
18 // Construct a message (see https://docs.expo.io/push-notifications/sending-notifications/)
19 messages.push({
20 to: pushToken,
21 sound: "default",
22 title,
23 body,
24 data,
25 })
26 }
27
28 // The Expo push notification service accepts batches of notifications so
29 // that you don't need to send 1000 requests to send 1000 notifications. We
30 // recommend you batch your notifications to reduce the number of requests
31 // and to compress them (notifications with similar content will get
32 // compressed).
33 let chunks = expo.chunkPushNotifications(messages)
34 let tickets = []
35 ;(async () => {
36 // Send the chunks to the Expo push notification service. There are
37 // different strategies you could use. A simple one is to send one chunk at a
38 // time, which nicely spreads the load out over time:
39 for (let chunk of chunks) {
40 try {
41 let ticketChunk = await expo.sendPushNotificationsAsync(chunk)
42 console.log(ticketChunk)
43 tickets.push(...ticketChunk)
44 // NOTE: If a ticket contains an error code in ticket.details.error, you
45 // must handle it appropriately. The error codes are listed in the Expo
46 // documentation:
47 // https://docs.expo.io/push-notifications/sending-notifications/#individual-errors
48 } catch (error) {
49 console.error(error)
50 }
51 }
52 })()
53
54 // Later, after the Expo push notification service has delivered the
55 // notifications to Apple or Google (usually quickly, but allow the the service
56 // up to 30 minutes when under load), a "receipt" for each notification is
57 // created. The receipts will be available for at least a day; stale receipts
58 // are deleted.
59 //
60 // The ID of each receipt is sent back in the response "ticket" for each
61 // notification. In summary, sending a notification produces a ticket, which
62 // contains a receipt ID you later use to get the receipt.
63 //
64 // The receipts may contain error codes to which you must respond. In
65 // particular, Apple or Google may block apps that continue to send
66 // notifications to devices that have blocked notifications or have uninstalled
67 // your app. Expo does not control this policy and sends back the feedback from
68 // Apple and Google so you can handle it appropriately.
69 let receiptIds = []
70 for (let ticket of tickets) {
71 // NOTE: Not all tickets have IDs; for example, tickets for notifications
72 // that could not be enqueued will have error information and no receipt ID.
73 if (ticket.id) {
74 receiptIds.push(ticket.id)
75 }
76 }
77
78 let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds)
79 ;(async () => {
80 // Like sending notifications, there are different strategies you could use
81 // to retrieve batches of receipts from the Expo service.
82 for (let chunk of receiptIdChunks) {
83 try {
84 let receipts = await expo.getPushNotificationReceiptsAsync(chunk)
85 console.log(receipts)
86
87 // The receipts specify whether Apple or Google successfully received the
88 // notification and information about an error, if one occurred.
89 for (let receiptId in receipts) {
90 let { status, message, details } = receipts[receiptId]
91 if (status === "ok") {
92 continue
93 } else if (status === "error") {
94 console.error(
95 `There was an error sending a notification: ${message}`
96 )
97 if (details && details.error) {
98 // The error codes are listed in the Expo documentation:
99 // https://docs.expo.io/push-notifications/sending-notifications/#individual-errors
100 // You must handle the errors appropriately.
101 console.error(`The error code is ${details.error}`)
102 }
103 }
104 }
105 } catch (error) {
106 console.error(error)
107 }
108 }
109 })()
110}

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

token.js
1const mongoose = require("mongoose")
2const Schema = mongoose.Schema
3const Token = new Schema({
4 tokenValue: {
5 type: String,
6 default: "",
7 },
8})
9
10module.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:

index.js
1const express = require("express")
2const cors = require("cors")
3const bodyParser = require("body-parser")
4const sendPushNotification = require("./utils/expo")
5
6if (process.env.NODE_ENV !== "production") {
7 // Load environment variables from .env file in non prod environments
8 require("dotenv").config()
9}
10
11require("./utils/connectdb")
12
13const Token = require("./models/token")
14
15const app = express()
16app.use(bodyParser.json())
17
18// Add the client URL to the CORS policy
19const whitelist = process.env.WHITELISTED_DOMAINS
20 ? process.env.WHITELISTED_DOMAINS.split(",")
21 : []
22const corsOptions = {
23 origin: function (origin, callback) {
24 if (!origin || whitelist.indexOf(origin) !== -1) {
25 callback(null, true)
26 } else {
27 callback(new Error("Not allowed by CORS"))
28 }
29 },
30 credentials: true,
31}
32
33app.use(cors(corsOptions))
34
35app.get("/", function (req, res) {
36 res.send({ status: "success" })
37})
38
39app.post("/send_notification", function (req, res) {
40 const { title, body, data, to } = req.body
41 if (to === "all") {
42 Token.find({}, (err, allTokens) => {
43 if (err) {
44 res.statusCode = 500
45 res.send(err)
46 }
47
48 const tokens = allTokens.map(token => {
49 return token.tokenValue
50 })
51
52 sendPushNotification(tokens, title, body, data)
53 res.send({ status: "success" })
54 })
55 } else {
56 sendPushNotification([to], title, body, data)
57 res.send({ status: "success" })
58 }
59})
60app.post("/save_token", function (req, res) {
61 const token = req.body.token
62
63 if (token) {
64 Token.find({ tokenValue: token }, (err, existingToken) => {
65 if (err) {
66 res.statusCode = 500
67 res.send(err)
68 }
69 if (!err && existingToken.length === 0) {
70 const newToken = new Token({ tokenValue: req.body.token })
71
72 newToken.save(function (err, savedToken) {
73 if (err) {
74 res.statusCode = 500
75 res.send(err)
76 }
77
78 res.send({ status: "success" })
79 })
80 } else {
81 res.send({ status: "success" })
82 }
83 })
84 } else {
85 res.statusCode = 400
86 res.send({ message: "token not passed!" })
87 }
88})
89
90app.get("/all_tokens", function (req, res) {
91 Token.find({}, (err, allTokens) => {
92 if (err) {
93 res.statusCode = 500
94 res.send(err)
95 }
96 res.send(
97 allTokens.map(token => {
98 // remove unnecessary fields
99 return { value: token.tokenValue }
100 })
101 )
102 })
103})
104
105// Start the server in port 8081
106const server = app.listen(process.env.PORT || 8081, function () {
107 const port = server.address().port
108 console.log("App started at port:", port)
109})

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:

1npx create-react-app push-api-client

Install BlueprintJS to style the page:

1yarn add @blueprintjs/core

Now import BlueprintJS css in index.css

index.css
1@import "~normalize.css";
2@import "~@blueprintjs/core/lib/css/blueprint.css";
3@import "~@blueprintjs/icons/lib/css/blueprint-icons.css";
4
5body {
6 margin: 0 auto;
7 max-width: 400px;
8}

Now update the App.js with the following code:

App.js
1import {
2 Card,
3 Button,
4 FormGroup,
5 InputGroup,
6 H2,
7 TextArea,
8 Intent,
9 HTMLSelect,
10 Toaster,
11 Position,
12} from "@blueprintjs/core"
13import React, { useEffect, useRef, useState } from "react"
14const allOption = [{ value: "all", label: "All" }]
15
16// Replace it with your own endpoint
17const API_ENDPOINT = "https://react-native-push-api.herokuapp.com/"
18
19function App() {
20 const [title, setTitle] = useState("")
21 const [body, setBody] = useState("")
22 const [data, setData] = useState()
23 const [recipients, setRecipients] = useState(allOption)
24 const [to, setTo] = useState("all")
25 const [isSubmitting, setIsSubmitting] = useState(false)
26 const toastRef = useRef()
27
28 useEffect(() => {
29 fetch(API_ENDPOINT + "all_tokens").then(async response => {
30 if (response.ok) {
31 const tokens = await response.json()
32 setRecipients(allOption.concat(tokens))
33 }
34 })
35 }, [])
36
37 const formSubmitHandler = e => {
38 let parsedData = {}
39 try {
40 parsedData = data ? JSON.parse(data) : {}
41 } catch (err) {
42 console.log(err)
43 }
44
45 e.preventDefault()
46
47 setIsSubmitting(true)
48 fetch(API_ENDPOINT + "send_notification", {
49 method: "POST",
50 headers: { "Content-Type": "application/json" },
51 body: JSON.stringify({
52 to,
53 title,
54 body,
55 data: parsedData,
56 }),
57 })
58 .then(async response => {
59 setIsSubmitting(false)
60
61 if (response.ok) {
62 toastRef.current.show({
63 icon: "tick",
64 intent: Intent.SUCCESS,
65 message: "Notification sent successfully.",
66 })
67 } else {
68 toastRef.current.show({
69 icon: "warning-sign",
70 intent: Intent.DANGER,
71 message: "Something went wrong.",
72 })
73 }
74 })
75 .catch(err => {
76 setIsSubmitting(false)
77 toastRef.current.show({
78 icon: "warning-sign",
79 intent: Intent.DANGER,
80 message: "Something went wrong.",
81 })
82 })
83 }
84 return (
85 <Card elevation="1">
86 <Toaster
87 ref={toastRef}
88 autoFocus={false}
89 canEscapeKeyClear={true}
90 position={Position.TOP}
91 usePortal={true}
92 />
93 <H2>Send Push Notification</H2>
94 <form className="notification-form" onSubmit={formSubmitHandler}>
95 <FormGroup label="Notification Title" labelFor="title">
96 <InputGroup
97 id="title"
98 placeholder="Notification Title"
99 type="text"
100 value={title}
101 onChange={e => setTitle(e.target.value)}
102 />
103 </FormGroup>
104 <FormGroup label="Notification Body" labelFor="body">
105 <InputGroup
106 id="body"
107 placeholder="Notification Body"
108 type="text"
109 value={body}
110 onChange={e => setBody(e.target.value)}
111 />
112 </FormGroup>
113 <FormGroup label="Additional Data" labelFor="data">
114 <TextArea
115 growVertically={true}
116 large={true}
117 placeholder="Additional data in JSON"
118 id="data"
119 value={data}
120 onChange={e => setData(e.target.value)}
121 className="bp3-fill"
122 />
123 </FormGroup>
124 <FormGroup label="Send To" labelFor="data">
125 <HTMLSelect
126 fill
127 options={recipients}
128 value={to}
129 onChange={e => setTo(e.target.value)}
130 />
131 </FormGroup>
132 <Button
133 intent="primary"
134 fill
135 type="submit"
136 text={isSubmitting ? "Sending" : "Send"}
137 />
138 </form>
139 </Card>
140 )
141}
142export default App

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:

App.js
1import React, { useEffect, useState } from "react"
2import { Button, StyleSheet, View, TextInput } from "react-native"
3import * as Notifications from "expo-notifications"
4import * as Permissions from "expo-permissions"
5
6// Show notifications when the app is in the foreground
7Notifications.setNotificationHandler({
8 handleNotification: async () => {
9 return {
10 shouldShowAlert: true,
11 }
12 },
13})
14
15export default function App() {
16 const [title, setTitle] = useState()
17 const [body, setBody] = useState()
18 const [token, setToken] = useState()
19
20 useEffect(() => {
21 // Permission for iOS
22 Permissions.getAsync(Permissions.NOTIFICATIONS)
23 .then(statusObj => {
24 // Check if we already have permission
25 if (statusObj.status !== "granted") {
26 // If permission is not there, ask for the same
27 return Permissions.askAsync(Permissions.NOTIFICATIONS)
28 }
29 return statusObj
30 })
31 .then(statusObj => {
32 // If permission is still not given throw error
33 if (statusObj.status !== "granted") {
34 throw new Error("Permission not granted")
35 }
36 })
37 .then(() => {
38 return Notifications.getExpoPushTokenAsync()
39 })
40 .then(response => {
41 const deviceToken = response.data
42 fetch("https://react-native-push-api.herokuapp.com/save_token", {
43 method: "POST",
44 headers: {
45 Accept: "application/json",
46 "Content-Type": "application/json",
47 },
48 body: JSON.stringify({
49 token: deviceToken,
50 }),
51 })
52 .then(() => {
53 console.log("Token saved!")
54 })
55 .catch(err => {
56 console.log(err)
57 })
58 })
59 .catch(err => {
60 return null
61 })
62 }, [])
63
64 useEffect(() => {
65 const receivedSubscription = Notifications.addNotificationReceivedListener(
66 notification => {
67 console.log("Notification Received!")
68 console.log(notification)
69 }
70 )
71
72 const responseSubscription =
73 Notifications.addNotificationResponseReceivedListener(response => {
74 console.log("Notification Clicked!")
75 console.log(response)
76 })
77 return () => {
78 receivedSubscription.remove()
79 responseSubscription.remove()
80 }
81 }, [])
82
83 const triggerLocalNotificationHandler = () => {
84 Notifications.scheduleNotificationAsync({
85 content: {
86 title: "Local Notification",
87 body: "Hello this is a local notification!",
88 },
89 trigger: { seconds: 5 },
90 })
91 }
92
93 const triggerPushNotificationHandler = () => {
94 fetch("https://exp.host/--/api/v2/push/send", {
95 method: "POST",
96 headers: {
97 Accept: "application/json",
98 "Accept-Encoding": "gzip,deflate",
99 "Content-Type": "application/json",
100 },
101 body: JSON.stringify({
102 to: token,
103 title,
104 body,
105 }),
106 })
107 }
108
109 return (
110 <View style={styles.container}>
111 <Button
112 title="Trigger Local Notification"
113 onPress={triggerLocalNotificationHandler}
114 />
115 <TextInput
116 style={styles.textInput}
117 value={title}
118 placeholder="Title"
119 onChangeText={setTitle}
120 />
121 <TextInput
122 style={styles.textInput}
123 value={body}
124 placeholder="Body"
125 onChangeText={setBody}
126 />
127 <TextInput
128 style={styles.textInput}
129 value={token}
130 placeholder="Token"
131 onChangeText={setToken}
132 />
133 <Button
134 title="Trigger Push Notification"
135 onPress={triggerPushNotificationHandler}
136 />
137 </View>
138 )
139}
140
141const styles = StyleSheet.create({
142 container: {
143 flex: 1,
144 backgroundColor: "#fff",
145 alignItems: "center",
146 justifyContent: "center",
147 },
148 textInput: {
149 borderBottomWidth: 1,
150 padding: 5,
151 margin: 15,
152 width: "80%",
153 },
154})

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

If you have liked article, stay in touch with me by following me on twitter.

© 2024 CodingDeft.Com