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!

  • alexdonvour
    says:

    Thanks for this tutorials , I have project which I want to send notifications to guest not to register user, so I will add table contain IP and browser agent and connect this modal with subscriber table , but I have one unclear idea how can I remove endpoint from subscriber table when user chose block after they allow it with in browser you can see the image to be more clear http://nimb.ws/A7NPyR
    Thanks in advance

    • mastashake08
      says:

      Webworkers have an event that can handle the uninstall event. You would have to do an http post after that event is called. I’m sure there is another way but I can’t think of any off top.

  • Pradip Rathod
    says:

    Hello Jyrone Parker,

    I am not getting push notification on mobile browsers. Can you please provide the solution.

    Thank You.

  • Felipe Marques
    says:

    Hi Jyrone, whats up ?

    Firstly, thankyou for this tutorial. This help me a lot. But, i think have one error in the begin.
    The class that you inform to put into providers array are wrong, because are like this: NotificationChannelsWebPushWebPushServiceProvider::class,

    Maybe, the correct is: NotificationChannels\WebPush\WebPushServiceProvider::class,

    Thanks again!

  • Hybrid
    says:

    I am using uuid for my user id’s as well as a custom user table and this is causing some odd issues.

    What areas do i need to change to get this working again as i see several references back and forth.

    • Dhivya
      says:

      I have this error

      Your requirements could not be resolved to an installable set of packages.

      Problem 1
      – pusher/pusher-php-server v3.0.1 requires ext-curl * -> the requested PHP extension curl is missing from your system.
      – pusher/pusher-php-server 3.0.0 requires ext-curl * -> the requested PHP extension curl is missing from your system.
      – Installation request for pusher/pusher-php-server ^3.0 -> satisfiable by pusher/pusher-php-server[3.0.0, v3.0.1].

  • kalesha
    says:

    hey im getting error like this

    Symfony\Component\Debug\Exception\FatalThrowableError: Call to undefined method NotificationChannels\WebPush\WebPushMessage::create() in file /var/www/html/push/app/Notifications/GenericNotification.php on line 42

    can you please solve this

  • Nigam Solanki
    says:

    i want to notification send like facebook. when i commented on another user photo and that user get notification
    using popup(“abc user is commented on your photos.”)

Leave a Reply

Instagram

Follow Me!

%d bloggers like this: