Web Push Notifications Using Django + React + Firebase Cloud Messaging

shaphat-writes
Stackademic
Published in
8 min readDec 5, 2023

--

What is web push notification?

Web push notification is a brief message that websites send to their users’ browser even if they are not actively browsing the website or not even using the browser.

Web push notifications are essential if you want to send important information, quick reminders or alerts to your users.

Push notifications are widely used in mobile apps for notifying users. In this article, we will look at how to send web based push notifications to the users’ browser.

Pre-requisites

Python and Django— You should be familiar with Python and Django in order to follow along the tutorial. However, you can apply all procedures outlined in totally different backend framework following the framework’s syntax.

JavaScript and Reactjs — You should be familiar with JavaScript and Reactjs in order to follow along with the tutorial but that doesn’t stop you from applying the procedure in other frontend frameworks.

Let’s jump in!

CHAPTER ZERO — Server side

We are going to set up a simple Django app where users can create a blog post. Mind you, I am not starting a Django project from scratch. I will only focus on the relevant code that we need to write.

# models.py in blog_app directory

from django.db import models

class Blog(models.Model):
owner = models.Foreignkey(User, on_delete=models.CASCADE) #we are not using a real User model in this article. You can use the User model specified in your application.
title = models.CharField(max_length=255, blank=False, null=False)
body = models.TextField(blank=False, null=False)

def __str__(self):
return f'{self.title}'

class PushNotificationToken(models.Model):
owner = models.Foreignkey(User, on_delete=models.CASCADE) #we are not using a real User model in this article. You can use the User model specified in your application.
fcm_token = models.CharField(max_length=99999999, unique=True)
# views.py in blog_app directory

from rest_frameowrk.response import Response
from rest_framework.decorators import api_view
from . models import Blog
from . serializers import BlogSerializer

# create a new blog post using function based view

@api_view(['POST'])
def create_blog_post(request):
if request.method == "POST":
serializer = BlogSerializer(data=request.data)
else:
status_code = 400
message = "The request is not valid"
return Response({"status_code": status_code, "message": message})
if serializer.is_valid():
serializer.save()
else:
status_code = 400
message = "There are errors in entries made."
return Response({"status_code": status_code, "message": message})
return Response({"status_code": 201, "message": "Blog post created successfully."})


@api_view(['POST'])
def create_fcm_token(request):
if request.method == "POST":
serializer = PushNotificationTokenSerializer(data=request.data)
else:
status_code = 400
message = "The request is not valid"
return Response({
"status_code": status_code,
"message": message
})

if serializer.is_valid():
serializer.save()
else:
status_code = 400
message = "There are errors in entries made."
return Response({
"status_code": status_code,
"message": message
})
return Response({
"status_code": 201,
"message": "fcm token created"
})


@api_view(['GET'])
def view_token(request, owner):
token = PushNotificationToken.objects.get(owner=owner)
serializer = PushNotificationTokenSerializer(token, many=False)
return Response(serializer.data)


@api_view(['PUT'])
def update_fcm_token(request, owner):
queryset = PushNotificationToken.objects.get(owner=owner)
if request.method == 'PUT':

serializer = PushNotificationTokenSerializer(instance=queryset, data=request.data, many = False, partial=True)
else:
status_code = 400
message = "The request is not valid"
return Response({
"status_code": status_code,
"message": message
})

if serializer.is_valid():
serializer.save()
else:
status_code = 400
message = "There is an error in the entry made"
return Response({
"status_code": status_code,
"message": message
})
return Response(
{
"status_code": 201,
"message": "Product updated successfully"
}
)
# serializers.py in blog_app directory

from rest_framework import serializers
from . models import Blog

class BlogSerializer(serializers.ModelSerializer):
class Meta:
models = Blog
fields = '__all__'

class PushNotificationTokenSerializer(serializers.ModelSerializer):
class Meta:
model = PushNotificationToken
fields = '__all__'

# Create any preferred url in your urls.py for the various API endpoints.

Before we continue writing the server side code, let us set up Firebase cloud messaging credentials for our web app.

Setting up firebase cloud messaging

  1. Visit https://console.firebase.google.com and create a new project.
  2. Click on the settings button beside “Project Overview” and select “Project settings”
  3. In the “Cloud Messaging” tab, click on “Manage Service Accounts” link under Firebase Cloud Messaging API (V1)
  4. In the new page that opens up, you should see an email that looks like: “firebase-adminsdk-omxda@yourprojectname.gserviceaccount.com”. Click on it.
  5. In the new page that opens up, click on the “KEYS” tab. In the “KEYS” tab, click on “ADD KEY” button and select “Create new key”. Select “json” from the options that pop up. Wait for a json file to get downloaded.
  6. The json file that gets downloaded should be similar to this:
{
"type": "service_account",
"project_id": "**********",
"private_key_id": "**********************",
"private_key": "-----BEGIN PRIVATE KEY-----\*******IBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCslg97SnS0HXAI\nLLQK+yLZ9xYXhVKbSa3B+67IZIi6dRtF0+hAGVjDCDCpfLWQd/lRgu8FO1zQVWT6\nMlyMVSkipOuqPUzlfTO6/gJBOZLzV2SrX4s7GC9nIHY07qLXgUl4m2eAk6XNyGac\nxm34/NN26Ne70i4WpYpCzdDaUAksWoidJnnyE1aQ1CJC+plwZHv1/yZ6nqxJIm34\nza0abvMvi6Jf0SHXLVPdZrtJVMGub74XJzF8VZLVQ9Tb9GRNZ5MWa1dNeyIM5pru\nhYnR6MRpLoYXjHxZCzOl8kX9eg5DAHtSA0JwCo6dxsbYbGAKEKuyrxXeKFxeSU0L\nUcnHr38bAgMBAAECggEAE0Bn+LJNA1x3R0opSUJLWHICAlyqYs0ct1NKY2snf1kZ\n0je5pBDPwG208+sH29YuNwP6gqRlDY5BBdHBVhwXyxgHe8V7wuus7hJwkPMJq+BX\nR/CP+OcRPpk15mCqRIzU88GuEX8m0yYkIB/YW5pkQlLl4bsnzfnoStxnjDq6UzqU\n5x/kUQE2g8Lq9k7y49gfIKIk6HuYmi7m1pzfYkbsPqqh3qVpMA1sefCVbhZY0Y/V\nlE1zOWWeL0XctL9/AiDdoJR8Laa8IqK3EosNtPGhLWWpH1AdAcrTe6Phg9YgWHJw\nLMTJBC1d0hNpp26RdjbJXhLv6E8rPJcU/GWmgRGZHQKBgQDr1PjlgLBVvjWW5K/i\nTlblh8bUunhAw/6vjmW03KSx2iKS1oQXeiAVMnXHi92/SB3TSIPUP8mzBXpZsI9D\nNvAi/GHbOrN3cNfP0w/qLdgh1msiY7qrpUW+XV9bLTdfpXxuy+pDRFKWyBtD9vYE\ndbNujC6jfD9dBJXMzbGnZtfylQKBgQC7WHW8nMFySEhW5zu8+IVrrpdf5LnyyQol\nEh44vVC5WumcZ40YLe5SogJuQzT37rXRqMzOpedophioaLjiNVblqNHL+t2YYx+h\nO+q3J9gKoPOFbQEzeWJq0HLwtNqVl59yQwEauzoc8DkZV248vHgB4xlEPMnLcchR\nCXqQNHhu7wKBgFNWLCo1wppaH+fVok2vb0enJl0QE+SXHg39nPU/rzdmJSeMhJsj\nPekfrr04MMEig9+g1W0QqX8IpYbCPK384PkMBKyK3taLWsgHBq2zS5gRhERfx5xW\nSAIQTt0SamnzObiReJQStbiwt+nZgHBtA15CTUzaYC3HrAP2gBvu3MrNAoGAPcZb\nfEgMGYzwHYe90P/5rpoxW/NlxUK5T6P7xyXVumjZ4zLZ+YEbtq+pMYaDrsVNusZ4\nUiOufHlYZB+z5xNDhhL2qtYbv6XfxiClsqM2v7p20iYxYTHDXAlD/U8FTJJkhx7E\n/HWEIgqsKUkFFo3m3Ghv6mpI+Aaa0O3ZNje3Bo0CgYAIWCM2GSCuFgBwKdPQuqlC\nrcrj4AMdfI7ptQjQrSJg6a7fL965kxWSuQMebT+JNVEIPDP4RTqvJXPEFV6KmPml\nFjCXbMFWK/OJV4XNDHZIXlZ5yDyMbG8DH59DjgXXRUElLlFQ66NcKCDpPHiNru2H\n1jg5QdjyxCpCZgkrzS0gWQ==\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-x7rs4@testpush-8c6ea.iam.gserviceaccount.com",
"client_id": "**********************",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-x7rs4%40testpush-8c6ea.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

// Note: I am replacing some of the values with asterisks for security reasons. Maintain the values in your json file.

We will use these credentials on the server side to generate an access token whenever we want to send a notification.

Server side code cont’d

# notifications.py in blog_app directory

from google.auth.transport.requests import Request
from google.oauth2 import service_account
import requests
import json

# We are generating an access_token to be used whenever we want to make a request to FCM
def generate_firebase_auth_key():
scopes = ['https://www.googleapis.com/auth/firebase.messaging']


# Replace the value of credentials_info with what you downloaded from Firebase cloud messaging
credentials_info = {
"type": "service_account",
"project_id": "**********",
"private_key_id": "**********************",
"private_key": "-----BEGIN PRIVATE KEY-----\*******IBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCslg97SnS0HXAI\nLLQK+yLZ9xYXhVKbSa3B+67IZIi6dRtF0+hAGVjDCDCpfLWQd/lRgu8FO1zQVWT6\nMlyMVSkipOuqPUzlfTO6/gJBOZLzV2SrX4s7GC9nIHY07qLXgUl4m2eAk6XNyGac\nxm34/NN26Ne70i4WpYpCzdDaUAksWoidJnnyE1aQ1CJC+plwZHv1/yZ6nqxJIm34\nza0abvMvi6Jf0SHXLVPdZrtJVMGub74XJzF8VZLVQ9Tb9GRNZ5MWa1dNeyIM5pru\nhYnR6MRpLoYXjHxZCzOl8kX9eg5DAHtSA0JwCo6dxsbYbGAKEKuyrxXeKFxeSU0L\nUcnHr38bAgMBAAECggEAE0Bn+LJNA1x3R0opSUJLWHICAlyqYs0ct1NKY2snf1kZ\n0je5pBDPwG208+sH29YuNwP6gqRlDY5BBdHBVhwXyxgHe8V7wuus7hJwkPMJq+BX\nR/CP+OcRPpk15mCqRIzU88GuEX8m0yYkIB/YW5pkQlLl4bsnzfnoStxnjDq6UzqU\n5x/kUQE2g8Lq9k7y49gfIKIk6HuYmi7m1pzfYkbsPqqh3qVpMA1sefCVbhZY0Y/V\nlE1zOWWeL0XctL9/AiDdoJR8Laa8IqK3EosNtPGhLWWpH1AdAcrTe6Phg9YgWHJw\nLMTJBC1d0hNpp26RdjbJXhLv6E8rPJcU/GWmgRGZHQKBgQDr1PjlgLBVvjWW5K/i\nTlblh8bUunhAw/6vjmW03KSx2iKS1oQXeiAVMnXHi92/SB3TSIPUP8mzBXpZsI9D\nNvAi/GHbOrN3cNfP0w/qLdgh1msiY7qrpUW+XV9bLTdfpXxuy+pDRFKWyBtD9vYE\ndbNujC6jfD9dBJXMzbGnZtfylQKBgQC7WHW8nMFySEhW5zu8+IVrrpdf5LnyyQol\nEh44vVC5WumcZ40YLe5SogJuQzT37rXRqMzOpedophioaLjiNVblqNHL+t2YYx+h\nO+q3J9gKoPOFbQEzeWJq0HLwtNqVl59yQwEauzoc8DkZV248vHgB4xlEPMnLcchR\nCXqQNHhu7wKBgFNWLCo1wppaH+fVok2vb0enJl0QE+SXHg39nPU/rzdmJSeMhJsj\nPekfrr04MMEig9+g1W0QqX8IpYbCPK384PkMBKyK3taLWsgHBq2zS5gRhERfx5xW\nSAIQTt0SamnzObiReJQStbiwt+nZgHBtA15CTUzaYC3HrAP2gBvu3MrNAoGAPcZb\nfEgMGYzwHYe90P/5rpoxW/NlxUK5T6P7xyXVumjZ4zLZ+YEbtq+pMYaDrsVNusZ4\nUiOufHlYZB+z5xNDhhL2qtYbv6XfxiClsqM2v7p20iYxYTHDXAlD/U8FTJJkhx7E\n/HWEIgqsKUkFFo3m3Ghv6mpI+Aaa0O3ZNje3Bo0CgYAIWCM2GSCuFgBwKdPQuqlC\nrcrj4AMdfI7ptQjQrSJg6a7fL965kxWSuQMebT+JNVEIPDP4RTqvJXPEFV6KmPml\nFjCXbMFWK/OJV4XNDHZIXlZ5yDyMbG8DH59DjgXXRUElLlFQ66NcKCDpPHiNru2H\n1jg5QdjyxCpCZgkrzS0gWQ==\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-x7rs4@testpush-8c6ea.iam.gserviceaccount.com",
"client_id": "**********************",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-x7rs4%40testpush-8c6ea.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

credentials = service_account.Credentials.from_service_account_info(
credentials_info, scopes=scopes
)

credentials.refresh(Request())

access_token = credentials.token
return access_token


#this function takes the auth_token(our access_token) and fcm_token(this will be generated from the client side.) as arguments.
# You can customize the message to your preference.
def send_push_notification(auth_token, fcm_token):
url = "https://fcm.googleapis.com/v1/projects/testpush-8c6ea/messages:send"

payload = json.dumps({
"message": {
"token": f'{fcm_token}',
"notification": {
"title": "New blog published!",
"body": "Hey, There is a new blog post you might want to check out."
},
"data": {
"key1": "value1",
"key2": "value2"
}
}
})
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {auth_token}'
}

response = requests.request("POST", url, headers=headers, data=pa

The next thing we are going to do on the server side is to use django signals to send a web push notification whenever a blog post is created.

# signals.py in blog_app directory

from django.db.models.signals import post_save
from django.dispatch import receiver
from . notifications import generate_firebase_auth_key, send_push_notification
from . models import Blog
from . serializers import BlogSerializer

@receiver(post_save, sender=Blog)
def blog_post_created_handler(sender, instance, created, **kwargs):
# We try to send a web push notification after a blog is created.
# We are making a pass in case it fails.
# You can log your error into a file or print to the terminal for easier debugging.
if created:
try:
owner = instance.owner
access_token = generate_firebase_auth_key()
target_browser = PushNotificationToken.objects.get(owner=owner)
fcm_token = target_browser.fcm_token
try:
send_push_notification(access_token, fcm_token)
except Exception as e:
pass
except:
pass

CHAPTER ONE — Client side

For our client side application, we will be using Reactjs with vite. We won’t start a Reactjs application from scratch. You can use an existing project or create a new one to follow along.

Create a file, “service-worker.js” in the “src” directory of your project.

// service-worker.js
self.addEventListener('push', event => {
const options = {
body: event.data.text(),
};

event.waitUntil(
self.registration.showNotification('Your Notification Title', options)
);
});

In your project’s entry file (main.js or index.js), add the code snippet below.

// index.js or your main entry file
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered with scope:', registration.scope);
})
.catch(error => {
console.error('Error registering Service Worker:', error);
});
}

Now let us initialze firebase to receive Firebase cloud messaging tokens and store them in the browser. In the src directory of your project, create a file, “firebaseInit.js”.

Before we jump into the code, we need to retrieve configuration credentials from firebase.

vapidKey

Visit https://console.firebase.google.com/ and click on ‘cloud messaging’ tab under your project settings. Scroll down to where you have ‘web push certificates’ and copy the key provided. This key would be used as your vapidKey.

firebaseConfig

Visit https://console.firebase.google.com. In the ‘General tab’ in your project settings, scroll down to the code snippet provided by firebase. Copy the value of your firebaseConfig. It should be similar to this:

firebaseConfig = {
apiKey: "******************************",
authDomain: "****************************",
projectId: "testpush-8c6ea",
storageBucket: "************************",
messagingSenderId: "195923469395",
appId: "***************************************",
measurementId: "**********************"
};

Now write the code below in your firebaseInit.js file.

import { initializeApp } from "firebase/app";
import { getMessaging, getToken } from "firebase/messaging";

// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "******************************",
authDomain: "****************************",
projectId: "testpush-8c6ea",
storageBucket: "************************",
messagingSenderId: "*******************",
appId: "***************************************",
measurementId: "**********************"
};

const app = initializeApp(firebaseConfig);

const messaging = getMessaging(app);



export const getFCMToken = async (setTokenFound) => {
let currentToken = "";

try {
currentToken = await getToken(messaging, { vapidKey: "BEFR2HyqI8M5cjpvNFHOlMgin3ArzACEWWURjvXYOjVyfNp0nBHnKlHQ1QoH5g4S41An0yMYsgK8VcALCrjdryk" });
if (currentToken) {
setTokenFound(true);
} else {
setTokenFound(false);
}
} catch (error) {
console.log("An error occurred while retrieving token. ", error);
}

return currentToken;
};

export const onMessageListener = () =>
new Promise((resolve) => {
messaging.onMessage((payload) => {
resolve(payload);
});
});

Finally, We are going to create a component that would be used to activate push notifications in the user’s browser.

In src/components, create a Notifications.js file and write the code below.

// CartComponent.js
import React, { useContext, useState, useEffect } from 'react'
import { getFCMToken } from "../firebaseInit.js";


const Notifications = (props) => {

const [userId, setUserId] = useState()
const [isTokenFound, setTokenFound] = useState(false);
const [fmcToken, setFCMToken] = useState("")
const [serverToken, setServerToken] = useState("")




let createFCMToken = async (theToken) => {
let response = await fetch(`api-endpoint-for creating fcm token`, {

method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"owner": userId,
"fcm_token": theToken

})
})

let data = await response.json()
console.log(data)

}

const userAndStoreData = async () => {
await getUserData()
console.log("user id",userId)
}


let updateServerToken = async (theToken) => {
let response = await fetch(`api-endpoint-for updating token`, {
method: 'PUT',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"fcm_token": theToken
})

})

let data = await response.json()
if (response.status === 201 || response.status === 200) {
window.location.reload()
} else {
alert('something went wrong')
}

}


let getServerToken = async () => {

let response = await fetch(`api-endpoint-for-user's-fcm-token/${userId}`, {
method: "GET",
credentials: "include",
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
})
let data = await response.json()
setServerToken(data.fcm_token)
console.log("server token", data.fcm_token)


}




const generateFCMToken = async () => {

let data;
data = await getTokenn(setTokenFound);
if (data) {
setFCMToken(data);
}
createFCMToken(data)


}


const reactivateFCMToken = async () => {

let data;
data = await getTokenn(setTokenFound);
if (data) {
setFCMToken(data);
}
updateServerToken(data)


}


useEffect(() => {

getServerToken()


}, [fmcToken]);

return (




<div>
{!serverToken.length ?
<button class="mt-3 group inline-flex w-full items-center justify-center rounded-md bg-[#0d0642] px-6 py-4 text-lg font-semibold text-white transition-all duration-200 ease-in-out focus:shadow " onClick={generateFCMToken}>Click to subscribe</button>
:
<><div class="mt-10">
<p class="mb-2 text-gray-500 text-center">You already have push notifications set up on a device. You can reactivate push notifications for current device using the button below</p>
</div>
<div className='flex justify-center'>
<button class="mt-3 group inline-flex items-center justify-center rounded-md bg-[#0d0642] px-6 py-4 text-lg font-semibold text-white transition-all duration-200 ease-in-out focus:shadow " onClick={reactivateFCMToken}>Re-activate push notifications</button>
</div></>
}
</di>

)
};

Notifications.propTypes = {};

export default Notifications;

CONCLUSION

Thank you for making it this far. I understand that the article may be confusing and difficult to understand. You can reach out to me on Twitter (https://twitter.com/shaphat_) with any challenges that you face.

Stackademic

Thank you for reading until the end. Before you go:

--

--