How to convert a React-Rails Web App to PWA, Push Notification using VAPID
Tags: webpushnotificatio, react, pushnotification, pwa
Published: 2020-03-22 17:26:28

---
title: How to convert a React-Rails Web App to PWA, Push Notification using VAPID
published: true
date: 2020-02-13 21:00:39 UTC
tags: web-push-notificatio,react,push-notification,pwa
canonical_url: https://medium.com/@bravemaster619/how-to-convert-a-react-ror-web-app-to-pwa-push-notification-using-vapid-ee33aba90f8a
---

### How to convert a React-RoR Web App to a PWA + Push Notification using VAPID

[In my previous article](https://dev.to/bravemaster619/react-ror-mongodb-how-to-decouple-frontend-and-backend-using-react-and-ruby-on-rails-5bpe-temp-slug-4430086), I described how to build a React web app with Ruby on Rails for backend and MongoDB for database.  
In this page, I am going to explain how to convert this web app into a [Progressive Web App](https://developers.google.com/web/progressive-web-apps). And I’ll show you how to implement push notification in our app using [VAPID](https://tools.ietf.org/html/draft-ietf-webpush-vapid-01).

If you haven’t read my previous article yet, please [read it first](https://dev.to/bravemaster619/react-ror-mongodb-how-to-decouple-frontend-and-backend-using-react-and-ruby-on-rails-5bpe-temp-slug-4430086). Or you can just [download the source code](https://github.com/bravemaster619/rails-react) of the previous article here and start reading this.

### What have we got?

When a user fills up subscription form and click “Subscribe” button, Axios will send a POST request to Rails API. Rails app will store the user information to database.

![](https://cdn-images-1.medium.com/proxy/1*n0sK2qWqlN6slItPEqZJVA.png)

After registration is done, React app will send a GET request to Rails Api. Rails Api will return JSON array of stored users in MongoDB. And React will render a user table that looks like the following picture:

![](https://cdn-images-1.medium.com/proxy/1*aV_y_FID0C_ojB46il-6hQ.png)

### What are we going to do?

It’s like this:

![](https://cdn-images-1.medium.com/max/764/1*Lnx04SUEv4-RwWOzrGTj9A.png)

When a registered user enters a message and clicks “Send” button, a push notification will show in other user’s machine.

![](https://cdn-images-1.medium.com/max/384/1*SGCR3fwpx42XbalK_iTAiw.png)

### How will it work?

When a user enters name and email, our app will request the user’s permission for push notifications. If the user allows it, a [service worker](https://developers.google.com/web/ilt/pwa/introduction-to-service-worker) will get a [subscription object](https://developer.mozilla.org/en-US/docs/Web/API/PushManager/getSubscription) which contains endpoint and keys. Rails Api will receive user information and subscription object from Axios and store them in the database.

![React+RoR+MongoDB+PWA+Web Push Notification, Step 1](https://cdn-images-1.medium.com/max/1024/1*iOH0Ic6pubWGrbYAn3mNtA.png)

Later, when the other registered user sends a message to another user, our Rails app will load target user’s subscription from database and send a POST request encrypted with Rails server’s keys to the [endpoint](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription/endpoint) of target user’s subscription . The endpoint will receive the request and send a push notification to target users’ machine.

![React+RoR+MongoDB+PWA+Web Push Notification, Step 2](https://cdn-images-1.medium.com/max/790/1*pV4hrX6Fb7iCuAOC475yZw.png)

### In Backend

#### 1. Add a dependency for web push

Google recommends using libraries when sending push notifications from a server:

> […web push libraries really shine, as the process of signing and sending a message can be quite complex](https://developers.google.com/web/ilt/pwa/introduction-to-push-notifications).

They prepared a [library list](https://github.com/web-push-libs) of push notifications for several languages. Unfortunately, there is no ruby library in there.

For that matter, [webpush](https://github.com/zaru/webpush) is a real lifesaver for ruby developers:

```ruby
# webpush for rails
gem 'webpush'
```

#### 2. Change user model

Since we need to save subscription information received from service worker to database, we have to change user model.

Change `models/user.rb` to this:

```ruby
class User
include Mongoid::Document
  field :name, type: String
  field :email, type: String
  field :subscription, type: Hash
end
```

Change `user_params` in `users_controller` like this:

```ruby
def user_params
  params.require(:user).permit(:name, :email, subscription: [:endpoint, :expirationTime, keys: [:p256dh, :auth]])
end
```

#### 3. Generate VAPID keys

> [Application servers SHOULD generate and maintain a signing key pair usable with elliptic curve digital signature (ECDSA) over the P-256 curve.](https://developers.google.com/web/ilt/pwa/introduction-to-push-notifications)

Cut and past the following lines to `config/application.rb`:

```ruby
require 'webpush' # This line goes to the head of the file

# One-time, on the server  
vapid_key = Webpush.generate_key

# Save these in your application server settings
puts "****** VAPID_PUBLIC_KEY *******"
puts vapid_key.public_key
puts "****** VAPID_PRIVATE_KEY *******"
puts vapid_key_.private_key
```

**Important** _: you need to require webpush in the head of the file._

Run the following commands in shell:

```console
$ bundle install
$ rails server
```

The console will output VAPID public key and private key:

```console
****** VAPID_PUBLIC_KEY *******
BL1IfYkFEXmhlVi5VrLIw0Tv_?????????????????????????????????????????ktz7miXzPjeSlWO33RyuuIRg=
****** VAPID_PRIVATE_KEY *******
vyAcYUZMsJRI8GkZnXh6???????????????????y210=
```

Create a file webpush.yml in config directory and save keys there:

```YAML
SUBJECT: mailto:johndoe@example.com
VAPID_PUBLIC_KEY: BL1IfYkFEXmhlVi5VrLIw0Tv_?????????????????????
????????????????????ktz7miXzPjeSlWO33RyuuIRg=
VAPID_PRIVATE_KEY: vyAcYUZMsJRI8GkZnXh6???????????????????y210=
```

Go back to `config/application.rb` and comment out the code snippet for key generation. Then add following lines to it:

```ruby
config.before_configuration do
   env_file = File.join(Rails.root, 'config', 'webpush.yml')
      YAML.load(File.open(env_file)).each do |key, value|
      ENV[key.to_s] = value
   end if File.exists?(env_file)
end
```

#### 4. Make a route and implement a method for push notification

Add the following line to `config/routes.rb`:

```ruby
post 'sendMessage', to: 'users#send_message'
```

Add the following lines to `app/controllers/users_controller.rb`:

```ruby
def send_message
  @message = params[:message]
  @user = User.find(params[:user_id])
  subscription = @user[:subscription]
  Webpush.payload_send(
      endpoint: subscription[:endpoint],
      message: @message,
      p256dh: subscription[:keys][:p256dh],
      auth: subscription[:keys][:auth],
      vapid: {
          subject: ENV['SUBJECT'],
          public_key: ENV['VAPID_PUBLIC_KEY'],
          private_key: ENV['VAPID_PRIVATE_KEY'],
          expiration: 12 * 60 * 60
      }
  )
  render json: { success: true }
end
```

**Important** _: you need to require webpush in the head of the file._

### In Frontend

#### 1. Customize default service worker of react-create-app

create-react-app already has a service worker for PWA. Unfortunately, there is nothing for push notification in serviceWorker.js. We are going to customize default service worker with [workbox-build](https://developers.google.com/web/tools/workbox/modules/workbox-build) and use that in our React app.

```console
$ npm i workbox-bulid
```

In src directory, create a file sw-build.js then cut and past the following lines:

```javascript
const workboxBuild = require('workbox-build');
// NOTE: This should be run *AFTER* all your assets are built
const buildSW = () => {
    // This will return a Promise
    return workboxBuild.injectManifest({
        swSrc: 'src/sw-template.js', // this is your sw template file
        swDest: 'build/service-worker.js', // this will be created in the build step
        globDirectory: 'build',
        globPatterns: [
            '**\/*.{js,css,html,png}',
        ]
    }).then(({count, size, warnings}) => {
        // Optionally, log any warnings and details.
        warnings.forEach(console.warn);
        console.log(`${count} files will be precached, totaling ${size} bytes.`);
    });
}

buildSW();
```

**_Note_** _: `sw-build.js` will auto generate a service worker as ordered in sw-template.js and dump the generated code to `build/service-worker.js`._

Then create a file `sw-template.js`:

```javascript
if ('function' === typeof importScripts) {
    importScripts(
        'https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js'
    );
    /* global workbox */
    if (workbox) {
        console.log('Workbox is loaded');

        /* injection point for manifest files.  */
        workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);

        /* custom cache rules*/
        workbox.routing.registerNavigationRoute('/index.html', {
            blacklist: [/^\/_/, /\/[^\/]+\.[^\/]+$/],
        });

        workbox.routing.registerRoute(
            /\.(?:png|gif|jpg|jpeg)$/,
            workbox.strategies.cacheFirst({
                cacheName: 'images',
                plugins: [
                    new workbox.expiration.Plugin({
                        maxEntries: 60,
                        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
                    }),
                ],
            })
        );

    } else {
        console.log('Workbox could not be loaded. No Offline support');
    }
}

self.addEventListener('notificationclose', function(e) {
    var notification = e.notification;
    var primaryKey = notification.data.primaryKey;

    console.log('Closed notification: ' + primaryKey);
});

self.addEventListener('notificationclick', function(e) {
    var notification = e.notification;
    var primaryKey = notification.data.primaryKey;
    var action = e.action;

    if (action === 'close') {
        notification.close();
    } else {
        clients.openWindow('https://github.com/bravemaster619');
        notification.close();
    }
});

self.addEventListener('push', function(e) {
    const title = (e.data && e.data.text()) || "Yay a message"
    var options = {
        body: 'This notification was generated from a push!',
        icon: 'images/example.png',
        vibrate: [100, 50, 100],
        data: {
            dateOfArrival: Date.now(),
            primaryKey: '2'
        },
        actions: [
            {action: 'explore', title: 'Learn more',
                icon: 'images/checkmark.png'},
            {action: 'close', title: 'Close',
                icon: 'images/xmark.png'},
        ]
    };
    e.waitUntil(
        self.registration.showNotification(title, options)
    );
});
```

**Note:** _Here in sw-template.js, we added event listeners for web push notification events._

And then modify scripts configuration in package.json as the following:

```javascript
"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build && npm run build-sw",
  "test": "react-scripts test",
  "eject": "react-scripts eject",
  "build-sw": "node ./src/sw-build.js"
},
```

#### 2. Change UserTable component

Modify `src/components/UserTable.jsx` as the following:

```javascript
import React from "react"
import { alertService } from '../services/alert'
import Axios from "axios"
import { API_HOST } from "../config"
class UserTable extends React.Component {

    constructor(props) {
        super(props)
        this.state={
            loading: true,
            users: []
        }
        this.changeMessage = this.changeMessage.bind(this)
        this.sendMessage = this.sendMessage.bind(this)
    }

    changeMessage(e, index) {
        const users = {...this.state.users}
        users[index].message = e.target.value
        this.setState(users)
    }

    sendMessage(e, index) {
        const users = {...this.state.users}
        const message = users[index].message
        if(!message) {
            alertService.showError("Please input message!")
            return
        }
        Axios.post(`${API_HOST}/sendMessage`, { message, user_id: users[index]['_id']['$oid'] }).then(res => {
            console.log(res.data.success)
            if(res.data.success) {
                alertService.showSuccess("Message sent!")
            } else {
                alertService.showError("Message did not send!")
            }
        }).catch(e => {
            console.error(e)
            alertService.showError("Message did not send!")
        })
    }

    componentDidMount() {
        Axios.get(`${API_HOST}/users`).then(res => {
            this.setState({
                users: res.data
            })
        }).catch(e => {
            alertService.showError('Cannot get user data...')
        }).finally(() => {
            this.setState({
                loading: false
            })
        })
    }

    render() {
        return (
            <div className="row mt-5 justify-content-center">
                <div className="col-12 col-lg-8">
                    <table className="table table-hover table-striped">
                        <thead>
                            <tr>
                                <th>Name</th>
                                <th>Email</th>
                                <th>Message</th>
                                <th/>
                            </tr>
                        </thead>
                        <tbody>
                        {this.state.loading ? (
                            <tr><td>Loading...</td></tr>
                        ) : (
                            <>
                                {this.state.users.map((user, index) => {
                                    return (
                                        <tr key={index}>
                                            <td>{user.name}</td>
                                            <td>{user.email}</td>
                                            <td>
                                                <input
                                                    type="text"
                                                    className="form-control"
                                                    onChange={(e) => this.changeMessage(e, index)}
                                                />
                                            </td>
                                            <td>
                                                <button
                                                    type="button"
                                                    className="btn btn-primary"
                                                    onClick={(e) => this.sendMessage(e, index)}
                                                >
                                                    Send
                                                </button>
                                            </td>
                                        </tr>
                                    )
                                })}
                                {!this.state.users.length && (
                                    <tr><td>Loading...</td></tr>
                                )}
                            </>
                        )}
                        </tbody>
                    </table>
                </div>
            </div>
        )
    }

}

export default UserTable
```

#### 3. Change Root Component

Modify `src/components/Root.jsx` as the following:

```javascript
import React from "react"
import Axios from "axios"
import { alertService } from '../services/alert'
import SubscribeForm from "./SubscribeForm"
import UserTable from "./UserTable"
import { API_HOST, VAPID_PUBLIC_KEY } from "../config"

class Root extends React.Component {

    constructor(props) {
        super(props)
        this.state = {
            name: '',
            email: '',
            sendingRequest: false,
            subscription: null,
        }
        this.changeName = this.changeName.bind(this)
        this.changeEmail = this.changeEmail.bind(this)
        this.subscribe = this.subscribe.bind(this)
    }

    changeName(e) {
        let name = e.target.value
        this.setState({name})
    }

    changeEmail(e) {
        let email = e.target.value
        this.setState({email})
    }

     urlBase64ToUint8Array(base64String) {
        const padding = '='.repeat((4 - base64String.length % 4) % 4);
        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;
    }

    subscribe() {
        if (!this.state.name) {
            return alertService.showError('Please input name!')
        }
        if (!this.state.email) {
            return alertService.showError('Please input email!')
        }
        if (!window.Notification) {
            return alertService.showError("You cannot use notification service")
        }
        if (!('serviceWorker' in navigator)) {
            return alertService.showError('Service worker not registered')
        }
        window.Notification.requestPermission().then(res => {
            if (res === "granted") {
                let context = this
                window.navigator.serviceWorker.ready.then(function (reg) {
                    reg.pushManager.subscribe({
                        userVisibleOnly: true,
                        applicationServerKey: context.urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
                    }).then(sub => {
                        Axios.post(`${API_HOST}/users`, {
                            name: context.state.name,
                            email: context.state.email,
                            subscription: sub
                        }).then(res => {
                            if (res.data && res.data._id) {
                                context.setState({
                                    subscription: sub
                                })
                            } else {
                                alertService.showError('Subscribing failed!')
                            }
                        })
                    })
                })
            } else {
                alertService.showError("You blocked notification.")
            }
        })
    }

    render() {
        return (
            <div className="container">
                {this.state.subscription ? (
                    <UserTable
                        subscription={this.state.subscription}
                    />
                ) : (
                    <SubscribeForm
                        name={this.state.name}
                        email={this.state.email}
                        changeName={this.changeName}
                        changeEmail={this.changeEmail}
                        subscribe={this.subscribe}
                        sendingRequest={this.state.sendingRequest}
                    />
                )}
            </div>
        )
    }

}

export default Root
```

#### 4. Add VAPID public key to React app

Modify `src/config.js` as the following:

```javascript
export const API_HOST = 'http://localhost:3000'
export const VAPID_PUBLIC_KEY= 'BL1IfYkFEXmhlVi5VrLIw0Tv_??????
???????????????????????????????????ktz7miXzPjeSlWO33RyuuIRg='
```

VAPID_PUBLIC_KEY was generated earlier by webpush gem.

#### 5. Enable service worker in React app

Go to `src/index.js` and change the following line:

```javascript
serviceWorker.unregister();
```

to this one:

```javascript
serviceWorker.register();
```

#### 6. Deploy React App to server

Since service worker [is running on only production environment](https://create-react-app.dev/docs/making-a-progressive-web-app/), it is a good idea to build our React app and host it to a server.

**_Note_** _: The built project must be located directly under the WebRoot directory, i.e._ [_http://localhost_](http://localhost) _is OK but_ [_http://localhost/rails-react-app_](http://localhost/rails-react-app) _won’t work._

![](https://cdn-images-1.medium.com/max/966/1*lUKkvXysQcZJqn9dI-POpw.png)

If you enter name and email and press “Subscribe” button the browser will ask your permission. Allow it and start sending messages!

![](https://cdn-images-1.medium.com/max/773/1*6SbK3NEHH7httqKdxvH80g.png)

![](https://cdn-images-1.medium.com/proxy/1*SGCR3fwpx42XbalK_iTAiw.png)

If you’re seeing this, well done! You implemented Push Notification in your web app! Notice that the deployed web app is PWA, too.

![](https://cdn-images-1.medium.com/max/739/1*qLF9IzGsnFbw2KQylaOTwg.png)

Note: PWA audit might not be fully passed in http. If you’re using https and fail on _http to https redirect test_, add the following to .htaccess file:

```
RewriteEngine On
RewriteCond %{SERVER\_PORT} !^443$
RewriteRule .\* https://%{SERVER\_NAME}%{REQUEST\_URI} [R,L]
```

### Useful Links

- You can get the working source code [here](https://github.com/bravemaster619/rails-react-pwa).
- You can read my [previous article](https://dev.to/bravemaster619/react-ror-mongodb-how-to-decouple-frontend-and-backend-using-react-and-ruby-on-rails-5bpe-temp-slug-4430086) of building a React web app with Rails Api.