Jyrone Parker

Implementing Web Push Using Laravel 5.4 Notifications

Web Push Is Awesome

No seriously! It’s a pivotal moment in web development. You see web push is a W3C protocol that allows websites to communicate with a user’s browser in the background, using this web developers can now do things such as: background push notifications, offline sync, and background analytics just to name few. The web push api follows the protocol and consist of 4 main stages:

  1. User Agent installs service worker
  2. App asks permission to show notifications
  3. Once permission is granted, subscribe to push
  4. Save subscription details on backend
  5. Send notification via Laravel notification

All the code can be seen on my Github and the live demo can be seen here!

Components Of Web Push

Implementing web push requires a true full-stack approach, on the back-end we have to:

  • Implement the WebPush notification channel
  • Set up VAPID keys (more details later)
  • Configure the user model to manage subscriptions
  • Fire the notifications

On the front end we must:

  • Create and install a service worker, the majority of our logic will be contained here and is responsible to handling push notifications
  • Give user prompt to accept permissions
  • Give user ability to send notification to self

I will break it down into back-end and front-end implementations as to not confuse you; let’s start with the back-end.

 

Creating The Backend

Create a blank Laravel application and run the following composer command to download the web push notification channel.

composer require laravel-notification-channels/webpush

Afterwards register in your config/app.php


// config/app.php 'providers' => [  
...  
NotificationChannelsWebPushWebPushServiceProvider::class,
 ],

Next open up your user model and add the following.

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use NotificationChannels\WebPush\HasPushSubscriptions;
class User extends Authenticatable
{
    use Notifiable, HasPushSubscriptions;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
}

This added HasPushSubscriptions trait allows the user model to receive push notifications.

Next publish the migration with:

php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="migrations"

Run the migrate command to create the necessary table:

php artisan migrate

You can also publish the config file with:

php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="config"

Generate the VAPID keys with (required for browser authentication) with:

php artisan webpush:vapid

This command will set VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEYin your .env file. VAPID is a web push protocol that is needed if we want to send push notifications. Basically it voluntarily identifies itself to a push notification server. If you want to read the specification you can here. Create the default auth routes

php artisan make:auth

Next let’s create the notification being used

php artisan make:notification GenericNotification

Open up the file and replace with the following contents

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use NotificationChannels\WebPush\WebPushMessage;
use NotificationChannels\WebPush\WebPushChannel;
class GenericNotification extends Notification
{
    use Queueable;
    public $title, $body;

    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct($title, $body)
    {
        //
        $this->title = $title;
        $this->body = $body;
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return [WebPushChannel::class];
    }

    public function toWebPush($notifiable, $notification)
    {
      $time = \Carbon\Carbon::now();
        return WebPushMessage::create()
            // ->id($notification->id)
            ->title($this->title)
            ->icon(url('/push.png'))
            ->body($this->body);
            //->action('View account', 'view_account');
    }

}

Next open up routes/api.php and fill out the API routes

<?php

use Illuminate\Http\Request;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

Route::post('/save-subscription/{id}',function($id, Request $request){
  $user = \App\User::findOrFail($id);

  $user->updatePushSubscription($request->input('endpoint'), $request->input('keys.p256dh'), $request->input('keys.auth'));
  $user->notify(new \App\Notifications\GenericNotification("Welcome To WebPush", "You will now get all of our push notifications"));
  return response()->json([
    'success' => true
  ]);
});

Route::post('/send-notification/{id}', function($id, Request $request){
  $user = \App\User::findOrFail($id);
  $user->notify(new \App\Notifications\GenericNotification($request->title, $request->body));
  return response()->json([
    'success' => true
  ]);
});

That’s it for the back end let’s move to the front end!

Setting Up The Service Worker

In order for the web application to handle notifications in the background, service workers must be implemented. A service worker is a javascript file that runs on a separate thread in the background (multi-threading FTW). These files must be installed by the front end application before they take effect. Open a new file public/js/service-worker.js and let’s implement a push listener.

self.addEventListener('push', function(event) {
  if (event.data) {
    var data = event.data.json();
    self.registration.showNotification(data.title,{
      body: data.body,
      icon: data.icon
    });
    console.log('This push event has data: ', event.data.text());
  } else {
    console.log('This push event has no data.');
  }
});

Now open up resources/views/home.blade.php and fill in the following

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Dashboard</div>

                <div class="panel-body">
                    <button class="btn btn-info" id="enable-notifications" onclick="enableNotifications()"> Enable Push Notifications </button>
                    <div class="form-group">
                    <input class="form-control" id="title" placeholder="Notification Title">
                    </div>
                    <div class="form-group">
                    <textarea id="body" class="form-control" placeholder="Notification body"></textarea>
                    </div>
                    <div class="form-group">
                    <button class="btn" onclick="sendNotification()">Send Notification</button>
                  </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
function sendNotification(){
  var data = new FormData();
data.append('title', document.getElementById('title').value);
data.append('body', document.getElementById('body').value);

var xhr = new XMLHttpRequest();
xhr.open('POST', "{{url('/api/send-notification/'.auth()->user()->id)}}", true);
xhr.onload = function () {
    // do something to response
    console.log(this.responseText);
};
xhr.send(data);
}
var _registration = null;
function registerServiceWorker() {
  return navigator.serviceWorker.register('js/service-worker.js')
  .then(function(registration) {
    console.log('Service worker successfully registered.');
    _registration = registration;
    return registration;
  })
  .catch(function(err) {
    console.error('Unable to register service worker.', err);
  });
}

function askPermission() {
  return new Promise(function(resolve, reject) {
    const permissionResult = Notification.requestPermission(function(result) {
      resolve(result);
    });

    if (permissionResult) {
      permissionResult.then(resolve, reject);
    }
  })
  .then(function(permissionResult) {
    if (permissionResult !== 'granted') {
      throw new Error('We weren\'t granted permission.');
    }
    else{
      subscribeUserToPush();
    }
  });
}

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

function getSWRegistration(){
  var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (_registration != null) {
    resolve(_registration);
  }
  else {
    reject(Error("It broke"));
  }
  });
  return promise;
}

function subscribeUserToPush() {
  getSWRegistration()
  .then(function(registration) {
    console.log(registration);
    const subscribeOptions = {
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        "{{env('VAPID_PUBLIC_KEY')}}"
      )
    };

    return registration.pushManager.subscribe(subscribeOptions);
  })
  .then(function(pushSubscription) {
    console.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
    sendSubscriptionToBackEnd(pushSubscription);
    return pushSubscription;
  });
}

function sendSubscriptionToBackEnd(subscription) {
  return fetch('/api/save-subscription/{{Auth::user()->id}}', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(subscription)
  })
  .then(function(response) {
    if (!response.ok) {
      throw new Error('Bad status code from server.');
    }

    return response.json();
  })
  .then(function(responseData) {
    if (!(responseData.data && responseData.data.success)) {
      throw new Error('Bad response from server.');
    }
  });
}


function enableNotifications(){
  //register service worker
  //check permission for notification/ask
  askPermission();
}
registerServiceWorker();
</script>
@endsection

You must run this from https:// or localhost else it will not work! Otherwise press the enable notifications button then send yourself a test! If you want the whole source code you can fork it here. If you enjoyed my content please like/subscribe/share! If you want another cool project learn how to automate your Twitter account!

34 Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Instagram

Follow Me!

Free Email Updates
Get the latest content first as well as EXCLUSIVE gifts, offers and discounts.
We respect your privacy.
%d bloggers like this: