DEV Community 👩‍💻👨‍💻

Cover image for Web-push Notification using React and Node js
ronyfr3
ronyfr3

Posted on • Updated on

Web-push Notification using React and Node js

I will share my code here for better understanding the core concept.

public/custom-sw.js

console.log("Service Worker Loaded...");
self.addEventListener('push', event => {
  const data = event.data.json()
  console.log('New notification', data)
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body:data.description,
      icon:data.icon
    })
  );
})
Enter fullscreen mode Exit fullscreen mode

src/serviceWorker.js

// This optional code is used to register a service worker.
// register() is not called by default.

// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.

// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA

const isLocalhost = Boolean(
    window.location.hostname === 'localhost' ||
      // [::1] is the IPv6 localhost address.
      window.location.hostname === '[::1]' ||
      // 127.0.0.1/8 is considered localhost for IPv4.
      window.location.hostname.match(
        /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
      )
  );

  export function register(config) {
    if ('serviceWorker' in navigator) {
      // The URL constructor is available in all browsers that support SW.
      const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
      if (publicUrl.origin !== window.location.origin) {
        // Our service worker won't work if PUBLIC_URL is on a different origin
        // from what our page is served on. This might happen if a CDN is used to
        // serve assets; see https://github.com/facebook/create-react-app/issues/2374
        return;
      }

      window.addEventListener('load', () => {
        const swFileName = process.env.NODE_ENV === 'production' ? 'service-worker.js' : 'custom-sw.js'
        const swUrl = `${process.env.PUBLIC_URL}/${swFileName}`;

        if (isLocalhost) {
          // This is running on localhost. Let's check if a service worker still exists or not.
          checkValidServiceWorker(swUrl, config);

          // Add some additional logging to localhost, pointing developers to the
          // service worker/PWA documentation.
          navigator.serviceWorker.ready.then(() => {
            console.log(
              'This web app is being served cache-first by a service ' +
                'worker. To learn more, visit https://bit.ly/CRA-PWA'
            );
          });
        } else {
          // Is not localhost. Just register service worker
          registerValidSW(swUrl, config);
        }
      });
    }
  }

  function registerValidSW(swUrl, config) {
    navigator.serviceWorker
      .register(swUrl)
      .then(registration => {
        registration.onupdatefound = () => {
          const installingWorker = registration.installing;
          if (installingWorker == null) {
            return;
          }
          installingWorker.onstatechange = () => {
            if (installingWorker.state === 'installed') {
              if (navigator.serviceWorker.controller) {
                // At this point, the updated precached content has been fetched,
                // but the previous service worker will still serve the older
                // content until all client tabs are closed.
                console.log(
                  'New content is available and will be used when all ' +
                    'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
                );

                // Execute callback
                if (config && config.onUpdate) {
                  config.onUpdate(registration);
                }
              } else {
                // At this point, everything has been precached.
                // It's the perfect time to display a
                // "Content is cached for offline use." message.
                console.log('Content is cached for offline use.');

                // Execute callback
                if (config && config.onSuccess) {
                  config.onSuccess(registration);
                }
              }
            }
          };
        };
      })
      .catch(error => {
        console.error('Error during service worker registration:', error);
      });
  }

  function checkValidServiceWorker(swUrl, config) {
    // Check if the service worker can be found. If it can't reload the page.
    fetch(swUrl)
      .then(response => {
        // Ensure service worker exists, and that we really are getting a JS file.
        const contentType = response.headers.get('content-type');
        if (
          response.status === 404 ||
          (contentType != null && contentType.indexOf('javascript') === -1)
        ) {
          // No service worker found. Probably a different app. Reload the page.
          navigator.serviceWorker.ready.then(registration => {
            registration.unregister().then(() => {
              window.location.reload();
            });
          });
        } else {
          // Service worker found. Proceed as normal.
          registerValidSW(swUrl, config);
        }
      })
      .catch(() => {
        console.log(
          'No internet connection found. App is running in offline mode.'
        );
      });
  }

  export function unregister() {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.ready.then(registration => {
        registration.unregister();
      });
    }
  }
Enter fullscreen mode Exit fullscreen mode

src/subscription.js

const convertedVapidKey = urlBase64ToUint8Array("YOUR_PUBLIC VAPID_KEY")

function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - base64String.length % 4) % 4)
  // eslint-disable-next-line
  const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/")

  const rawData = window.atob(base64)
  const outputArray = new Uint8Array(rawData.length)

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i)
  }
  return outputArray
}

function sendSubscription(subscription) {
  return fetch(`/notifications/subscribe`, {
    method: 'POST',
    body: JSON.stringify({
      subscription:subscription,
      title:"Notified by Precision Ordance",
      description:"someone buy a product",
      icon:"https://ichef.bbci.co.uk/news/976/cpsprodpb/9A50/production/_118740593_gettyimages-1231144196.jpg"
    }),
    headers: {
      'Content-Type': 'application/json'
    }
  })
}
//conditional render
let clicked = true

export function subscribeUser() {
  if(clicked) {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready.then(function(registration) {
      if (!registration.pushManager) {
        console.log('Push manager unavailable.')
        return
      }

      registration.pushManager.getSubscription().then(function(existedSubscription) {
        if (existedSubscription === null) {
          console.log('No subscription detected, make a request.')
          registration.pushManager.subscribe({
            applicationServerKey: convertedVapidKey,
            userVisibleOnly: true,
          }).then(function(newSubscription) {
            console.log('New subscription added.',newSubscription)
            sendSubscription(newSubscription)
          }).catch(function(e) {
            if (Notification.permission !== 'granted') {
              console.log('Permission was not granted.')
            } else {
              console.error('An error ocurred during the subscription process.', e)
            }
          })
        } else {
          console.log('Existed subscription detected.')
          sendSubscription(existedSubscription)
        }
      })
    })
      .catch(function(e) {
        console.error('An error ocurred during Service Worker registration.', e)
      })
  }
}else{
console.log('Can not reachable to the service worker');
}}
Enter fullscreen mode Exit fullscreen mode

src/App.JS

import './App.css';
import { subscribeUser } from './subscription';
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <button onClick={subscribeUser}>Click Here</button>
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import * as serviceWorker from './serviceWorker';
import { subscribeUser } from './subscription';

ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.register();

subscribeUser()
Enter fullscreen mode Exit fullscreen mode

src/package.json()
just install one package-->npm i cra-append-sw

"dependencies": {
    "@testing-library/jest-dom": "^5.15.0",
    "@testing-library/react": "^11.2.7",
    "@testing-library/user-event": "^12.8.3",
    "cra-append-sw": "^2.7.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3",
    "web-vitals": "^1.1.2"
  },
Enter fullscreen mode Exit fullscreen mode

Backend Code

index.js

const express = require('express')
const dotenv = require('dotenv')
const bodyParser = require('body-parser')
const cors = require('cors')
const webpush = require('web-push')

const app = express()

dotenv.config()

app.use(cors())
app.use(bodyParser.json())
###You can generate VAPID keys using the command:
**./node_modules/.bin/web-push generate-vapid-keys**
webpush.setVapidDetails("mailto: `YOUR EMAIL OR WEBSITE ADDRESS`", "YOUR-PUBLIC-VAPID-KEY", "YOUR-PRIVATE-VAPID-KEY")

app.get('/', (req, res) => {
  res.send('Hello world!')
})

app.post('/notifications/subscribe', (req, res) => {
  console.log(req.body);
  const payload = JSON.stringify({
    title:req.body.title,
    description:req.body.description,
    icon:req.body.icon
  })
// console.log(req.body.subscription);
  webpush.sendNotification(req.body.subscription, payload)
    .then(result => console.log())
    .catch(e => console.log(e.stack))

  res.status(200).json({'success': true})
});

app.listen(8000, () => console.log('The server has been started on the port 8000'))
Enter fullscreen mode Exit fullscreen mode

Top comments (16)

Collapse
 
suhakim profile image
sadiul hakim

I was looking for it

Collapse
 
ronyfr3 profile image
ronyfr3 Author

vhaiya kono help lagle boilen.Thanks

Collapse
 
suhakim profile image
sadiul hakim

Ah?are you bangli?me too.very nice.

Thread Thread
 
ronyfr3 profile image
ronyfr3 Author

ha vhaiya

Thread Thread
 
suhakim profile image
sadiul hakim

bangladesh na india?Bangladesh hole kon district?

Thread Thread
 
ronyfr3 profile image
ronyfr3 Author

chuadanga, Bangladesh

Thread Thread
 
suhakim profile image
sadiul hakim

Kustia,bd

Collapse
 
collimarco profile image
Marco Colli

If you use a service, it's even simpler.

Here's an example with React + Pushpad for example:
pushpad.xyz/docs/frontend_frameworks

Collapse
 
mravangi profile image
Mostafa Ravangi

how about nextjs app.

can you post an example?

Collapse
 
swanprince profile image
swan-prince • Edited on

I have followed this article.
But it returns error in backend.
dev-to-uploads.s3.amazonaws.com/up...
Could you help me?

Collapse
 
ronyfr3 profile image
ronyfr3 Author

yes sir, i can help you

Collapse
 
amitnadcab profile image
amit-nadcab

Having same error what to do?

Collapse
 
guepejuang profile image
Tehe~

can i se in full react app?

Collapse
 
ronyfr3 profile image
ronyfr3 Author

full react code is displayed!

Collapse
 
techpilot profile image
Stephen Ngwu

How do I make this to work on production?

Collapse
 
mijanur_rahman profile image
Mijanur Rahman

Thank you sir.

Visualizing Promises and Async/Await 🤯

async await

☝️ Check out this all-time classic DEV post