Create my own push message server

To use PN (Push Notification) I usually make use of servers in clouds such as Firebase, Azure, etc.

Now I've got a service to do and my client requires us to have our own server.

I saw this link in Soen, but if anyone has something simpler and wants to share, I appreciate it. Android Only.

Author: Lauro Moraes, 2017-12-25

2 answers

Implementing Progressive Web APP on your domain.

Minimum Requirements:

  1. the domain must be over https (ssl encryption).
  2. the domain must have a valid manifest file.
  3. the domain must pass a browser audit that I will describe below.

Well, to have web push notifications you must first turn your domain into a progressive web app. In addition to push notifications several other advantages are provided, such as static cache direct from the browser which allows you to navigate over the domain with the device offline, the addition of a shortcut icon directly inserted in the user's homescreen and navigation in the form of a mobile application without the dependence of a browser.

Registering a manifest file:

In the root of your domain you must include a json file let's call it a manifest.json in this example:

{
  "short_name"       : "teste",
  "name"             : "meudominio.com",
  "display"          : "standalone",
  "background_color" : "#131b26",
  "theme_color"      : "#131b26",
  "icons"            : [{  "src"  : "tema/BlueRED/images/48x48.png",
                           "type" : "image/png",
                           "sizes": "48x48"
                        },
                        {  "src"  : "tema/BlueRED/images/96x96.png",
                           "type" : "image/png",
                            "sizes": "96x96"
                        },
                        {  "src"  : "tema/BlueRED/images/192x192.png",
                           "type" : "image/png",
                           "sizes": "192x192"
                        },
                        {  "src"  : "tema/BlueRED/images/256x256.png",
                           "type" : "image/png",
                            "sizes": "256x256"
                        },
                        {  "src"  : "tema/BlueRED/images/512x512.png",
                           "type" : "image/png",
                           "sizes": "512x512"
                        }],
  "start_url": "/ utm_source=homescreen&utm_medium=pwa&utm_campaign=pwa"
} 

You will also need a large format image (512x512), to generate smaller images as in the sizes described in our manifesto.json above. These images are used by android to add icon to the user's homescreen depending on the size of the display. There are tools that scale the image for you just having an image in the format mentioned above so as not to lose the resolution. Also the web app creates the manifest alone for you, just fill out the form. Use this tool to create your manifest.json and already scaled images. (JSON Manifest Generator )

After generating your Manifest file you must indicate in the domain head that it exists by adding the following meta on all pages.

<link rel="manifest" href="/manifesto.json" />

Creating a file with your service worker's routines.

Definition by Google:

A service worker is a script that your browser executes second plan, separate from the Web page, enabling features that do not they need a Web page or user interaction. Currently, service workers already include features like push notifications and synchronization in the background.

Registering a sw file.js. (Our Service Worker)

This my solution is aimed at static caching of all the resources of the domain, which allows a navigation even if offline.

var CACHE_VERSION = 26;  
/* Esse é o número de versão do cache muito importante. Se mudar a versão 
força todos os navegadores que possuem nosso service worker registrado, a 
excluírem suas copias e solicitarem ao servidor uma nova copia dos arquivos 
cacheados. */
var CURRENT_CACHES = {
    prefetch: 'prefetch-cache-v' + CACHE_VERSION
};

self.addEventListener('install', function(event){
   /* Dentro dessa lista é adicionado todos os arquivos estáticos do domínio 
   um por um, antes que o usuário solicite a pagina todo esse conteúdo é 
   baixado para o navegador utilizando uma cabeça em background do 
   navegador. Você pode cachear páginas,imagens,css entre outras 
   coisas  que nem foram abertas ainda. 
   O resultado é um site absurdamente rápido, e  com o tempo de 
   permanência exageradamente alto. Alguns de meus domínio possuem 12 
   minutos ou mais de duração média da visita, assim como a taxa de rejeição 
   despencou*/ 
   var urlsToPrefetch = [ '/tema/BlueRED/images/pix.gif',
                           '/js/sprite.js',
                           '/tema/BlueRED/images/640X400.jpg' ];
    console.log('Handling install event. Resources to prefetch:', urlsToPrefetch);

    self.skipWaiting();

    event.waitUntil(
        caches.open(CURRENT_CACHES.prefetch).then(function(cache) {
            return cache.addAll(urlsToPrefetch);
        })
    );
});

self.addEventListener('activate', function(event){
/*Se este nome de cache não estiver presente na matriz de nomes de cache 
"esperados", então exclua-o.*/
    var expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) {
        return CURRENT_CACHES[key];
    });

    event.waitUntil(
        caches.keys().then(function(cacheNames){
            return Promise.all(
                cacheNames.map(function(cacheName) {
                    if (expectedCacheNames.indexOf(cacheName) === -1) {
                        console.log('Deleting out of date cache:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

self.addEventListener('fetch', function(event){
    console.log(event.request);
    if(!event.request.headers.get('range')){
        event.respondWith(
            /* caches.match  irá procurar um entrada no cache de browser 
            para a requisição feita para o arquivo estático interceptando 
            qualquer requisição até seu servidor, caso encontre ele responde 
            ao cliente localmente do navegador.*/               
            caches.match(event.request).then(function(response){
                if (response) {
                    /* Se encontrou uma resposta válida para o arquivo 
                    solicitado responde localmente com o cache */
                    console.log('Found response in cache:', response);
                    return response;
                }
                /* Caso não encontre o arquivo cacheado efetua uma 
                requisição até o servidor para solicitar o arquivo */
                console.log('No response found in cache. About to fetch from network...');
                // event.request will always have the proper mode set ('cors, 'no-cors', etc.) so we don't
                // have to hardcode 'no-cors' like we do when fetch()ing in the install handler.
                return fetch(event.request).then(function(response){
                    console.log('Response from network is:', response);
                    caches.open(CURRENT_CACHES.prefetch).then(function(cache){
                      if(response.type != 'opaque' && response.type != 'cors' && response.url != 'https://www.meudominio.com/sw.js'){
                        /* Se a requisição tiver sido efetuada essa rotina 
                       adiciona o arquivo ao cache, mas antes faz o devido 
                       tratamento para requisições opacas e o propio arquivo 
                       sw.js não ser cacheado.*/
                       console.log("cache adicionado:"+response.url);
                       cache.add(response.url);
                      }
                    });
                    return response;
                }).catch(function(error){
                    console.error('Fetching failed:', error);
                    throw error;
                });
            })
        );
    }
});       

Finally, there must be a javascript routine contained in all pages of your domain, so that on the first access of the user to the domain makes the registration of our service worker, if he has not been registered in this browser yet.

In this case we can implement as follows:

if('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
      navigator.serviceWorker.register('/sw.js').then(function(registration) 
     {
       // Registration was successful
       console.log('ServiceWorker registration successful with scope: ', 
       registration.scope);
     }).catch(function(err) {
          // registration failed :(
         console.log('ServiceWorker registration failed: ', err);
     });
    });
}

function waitUntilInstalled(registration){
   return new Promise(function(resolve, reject){
       if (registration.installing) {
           // If the current registration represents the "installing" service worker, then wait
           // until the installation step (during which the resources are pre-fetched) completes
           // to display the file list.
          registration.installing.addEventListener('statechange', function(e) {
           if (e.target.state === 'installed'){
              resolve();
           }else if (e.target.state === 'redundant'){
              reject();
           }
       });
     }else{
          /* Otherwise, if this isn't the "installing" service worker, then 
          installation must have been */
          /* completed during a previous visit to this page, and the resources are already pre-fetched.*/
          // So we can show the list of files right away.
          resolve();
     }
  });
}

if('serviceWorker' in navigator){
    navigator.serviceWorker.register('/sw.js',{
        scope: '/'
    }).then(waitUntilInstalled)
      //.then(showFilesList)
     .catch(function(error) {
       // Something went wrong during registration. The service-worker.js file
       // might be unavailable or contain a syntax error.
       document.querySelector('#status').textContent = error;
     });
 }else{
      // The current browser doesn't support service workers.
      var aElement         = document.createElement('a');
      aElement.href        = 'http://www.chromium.org/blink/serviceworker/service-worker-faq';
      aElement.textContent = 'Service workers are not supported in the current browser.';
      document.querySelector('#status').appendChild(aElement);
 }    

This routine is executed before the domain is opened, it is responsible for validating and registering the service worker for the domain. After all this done, you should open the console in google chrome and perform the validation of all the above steps by using the tool in the audits tab of the browser. If there is any problem with the implementation the browser debug itself will provide you with the documentation page with the explanation of the errors.

insert the description of the image here

After the PWA implemented, you can start the implementation of web push notifications, this documentation will be useful to you Push Notifications . Hope the humble post helps!

 7
Author: Rafael Salomão, 2017-12-27 13:01:26

Preface 1:

First it should be understood that the services of Push Notification in the mobile scope (mobile) are implemented by the software manufacturer (usually integrated via OS).

The basis of its operation is the "operating system Push Notification Service" (OSPNS). Each mobile operating system (OS), including iOS, Android, Firefox OS, Windows, BlackBerry among others have their own service. Browsers use the "endpoint" of these services (their own) to support API Web Push as described below.

The methodology of its implementation follows the concept Pub / Sub (subscription / publication) where, the device subscribes to a service (server) informing a unique id of the application and then this whistle to receive a publication (notification). For this the device constantly "observes" the service in order to check if there is (exists) a new publication (notification) to be delivered.

As mentioned earlier these services (servers) are "embedded" by the software manufacturer and the method as well as the frequency of verification are defined by the manufacturers.

For example we can list for example:

  • Apple APNS (Apple Push Notification Service) (iOS/Mac OS X)
  • Google GCM / FCM (Google Cloud Messaging / Firebase Cloud Messaging) (Android)
  • Mozila Autopush (Mozilla Push server and Push Endpoint) (Firefox/Firefox OS)
  • Windows WNS (Windows Push Notification Services) (UWP)
  • Microsoft MPNS (Microsoft Push Notification Service) (Windows Phone 8)
  • Amazon ADM (Amazon Device Messaging) (by SNS)
  • Baidu Cloud Push (Baidu Cloud Push SDK)
  • Tizen SPPCD2 (Samsung Push Service) (Tizen OS)

Notes:

  • A example from the above list APNS can be used both on the device (iOS/Mac OS X) when on a watchOS 3 or later... the system decides which device has received the notification. See more details in official documentation .

  • Autopush is used in Mozila's mobile system as well as to meet the Web Push specification in web applications using Firefox. See more details in official documentation .

  • Windows WNS is used in UWP (Universal Windows Platform) that integrates the connected services of the tenth (10) version of this OS.

Other mobile OS derivatives (e.g. Android) may use the services of their base source or provide their own service.


Implement your own service:

At the time of this publication (27/12/2017) I do not know the possibility of implementing a service for Native use of push notifications on a device from a remote source.

Using native functions for push requires registration in a service (hitherto proprietary), it is not possible to fire them locally (on the device) without being previously registered (from a registered source).

Alternatives:

There are many alternatives (push application servers) , some free others paid that in short will use some of the above services aforementioned.

These tools (push application servers) in turn use plugins or tools integrated into the SDK of the system in question to extend native classes and functionalities referring to the "push" services in order to handle the requests (say REST) of these services.

The great difference is that these tools provide some degree of automation (for example for marketing) as well as the possibility of management tools and analyze thing that by default is restricted to bad software manufacturers, as the message is encrypted these (proprietary) services work as a gatekeeper based on TTL (time-to-live) and message priority.

These tools also allow you to use a remote access (e.g. a web application server or via command line) to launch new notifications or in some cases obtain metrics (usage analysis data) via API.

Following the reasoning of these "tools" your application could maintain a connection to your server and launch a notification using "Insert [Android]"... this method would necessarily require a API Key GCM and could be used by both GCM and FCM or even the Cordova plugin (in the case of a Cordova application).

Example using "Insert" - (See commented code gist):

/* Firebase instruções */
public class InsertFirebaseMessagingService extends FirebaseMessagingService {    
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {    
        if ( remoteMessage.getNotification() != null ) {    
            RemoteMessage.Notification notification = remoteMessage.getNotification();    
            int iconResId = 0;
            final String drawableResourceName = notification.getIcon();
            if ( !TextUtils.isEmpty(drawableResourceName) ) {
                iconResId = getResources().getIdentifier(drawableResourceName, "drawable", getPackageName());
            }
            Uri sound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
            final String soundFileName = notification.getSound();    
            if ( !TextUtils.isEmpty(soundFileName) && !soundFileName.equals("default") ) {
                sound = Uri.parse("android.resource://" + this.getPackageName() + "/raw/" + soundFileName);
            }    
            Bundle bundle = new Bundle();    
            for (Map.Entry<String, String> entry : remoteMessage.getData().entrySet()) {
                bundle.putString(entry.getKey(), entry.getValue());
            }    
            NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
            .setContentTitle("Your App Name")
            .setContentText(notification.getBody())
            .setAutoCancel(true)
            .setSmallIcon(iconResId == 0 ? getApplicationInfo().icon : iconResId)
            .setSound(sound)
            .setContentIntent(Insert.generatePendingIntentForPush(bundle));    
            NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);    
            nm.notify(0, builder.build());
        }
    }
}

/* GCM instruções */
public class RegistrationIntentService extends IntentService {
    public RegistrationIntentService() {
        super("RegistrationIntentService");
    }
    @Override
    protected void onHandleIntent(Intent intent) {
        try {
            InstanceID instanceID = InstanceID.getInstance(this);
            String token = instanceID.getToken(getString(R.string.gcm_defaultSenderId), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
            Insert.setPushId(token);
        } catch (Exception e) {}
    }
} 
Intent intent = new Intent(this, RegistrationIntentService.class);
startService(intent);

public class InsertInstanceIDListenerService extends InstanceIDListenerService {

    @Override
    public void onTokenRefresh() {
        Intent intent = new Intent(this, RegistrationIntentService.class);
        startService(intent);
    }
}

public class InsertGcmListenerService extends GcmListenerService {    
    @Override
    public void onMessageReceived(String from, final Bundle data) {    
        int iconResId = 0;
        final String drawableResourceName =  data.getBundle("notification").getString("icon");
        if ( !TextUtils.isEmpty(drawableResourceName) ) {
            iconResId = getResources().getIdentifier(drawableResourceName, "drawable", getPackageName());
        }    
        Uri sound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        final String soundFileName = data.getBundle("notification").getString("sound");
        if ( !TextUtils.isEmpty(soundFileName) && !soundFileName.equals("default") ) {
            sound = Uri.parse("android.resource://" + this.getPackageName() + "/raw/" + soundFileName);
        }    
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
        .setContentTitle("Your App Name")
        .setContentText(data.getString("message"))
        .setAutoCancel(true)
        .setSmallIcon(iconResId == 0 ? getApplicationInfo().icon : iconResId)
        .setSound(sound)
        .setContentIntent(Insert.generatePendingIntentForPush(data));    
        NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        nm.notify(0, builder.build());
    }
}

/* Cordova (Push Notification plugin) instruções */

// Cordova command line:
cordova plugin add phonegap-plugin-push --variable SENDER_ID=<sender-id>
// Add the following code after "onDeviceReady" 
var push = PushNotification.init({
    "android": {
        "senderID": "<sender-id>"
    },
    "ios": {
        "alert": "true",
        "badge": "true",
        "sound": "true"
    },
    "windows": {}
 });
 push.on('registration', function(data) {
     window.plugins.Insert.setPushId(data.registrationId);
 });
 push.on('notification', function(data) {
     //...
 });
 push.on('error', function(e) {
     console.log(e.message);
 });

Source: support.insert.io

If the purpose is simply to notify the user your application (mobile) could communicate with a remote server through the use of WebSockets or some library as a third party and use a native notification function... "bad this would no longer be a push notification" !

Example (Android) - (See commented code gist )

NotificationCompat.Builder mBuilder =
    new NotificationCompat.Builder(this)
    .setSmallIcon(R.drawable.notification_icon)
    .setContentTitle("My notification")
    .setContentText("Hello World!");
Intent resultIntent = new Intent(this, ResultActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(ResultActivity.class);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent =
    stackBuilder.getPendingIntent(
        0,
        PendingIntent.FLAG_UPDATE_CURRENT
    );
mBuilder.setContentIntent(resultPendingIntent);
NotificationManager mNotificationManager =
    (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.notify(mId, mBuilder.build());

Source: developer.android.com

A example of this Facebook Messenger uses on Android and iOS its own gateway using MQTT to send and receive notifications.

Say on iOS, since apps can't run background services for too long, they use APNS when the app is closed and MQTT when opened.

Source: IBM .

There is also the possibility to create an " application server push"...

Push application servers:

There are a considerably good range (question of opinion) of open source projects that can serve to create your own push application service the following listing reflects a few:

  • rpush
    • written in: Ruby
    • support: APNS, FCM, ADM, WPNS
    • license: MIT
  • gorush
    • written in: Go
    • supports a: APNS, FCM
    • license: MIT
  • pushd
    • written in: CoffeeScript
    • support: APNS, GCM, WNS, MPNS, HTTP (POST), EventSource
    • license: MIT
  • uniqush-push
    • written in: Go
    • support: APNS, GCM, ADM
    • license: Apache-2.0

For specific use on Android your application (configured for Push) using FCM credentials would follow the following scheme:

push


Related: Web Push

A API Web Push is currently in the loop WD (draft) according to W3C , this API is designed to bring the functionality of services to web applications "Push Notification" and second caniuse.com on the current date (12/26/2017) is supported in the following Android system mobile browsers (in addition to desktop browsers):

insert the description of the image here

  • Opera Mobile (37)
  • Chrome for Android (62)
  • Firefox for Android (57)
  • UC for Android (11.4)
  • Samsung Internet (62)
  • QQ Browser (1.2)

To create a " application server push " that use Web Push There are a Large Offer of Open Source projects among which I would indicate the following repository that contains a collection of sources:

  • web-push-libs - this collection contains sources in:
    • C, PHP, JavaScript, Python, C# and Java (constantly updated)

However this API has its "peculiarities" such as the (mandatory) use of API Service Worker well as of user permissions to use the notifications of the browser.

Your specification requires the server to provide a key VAPID (Voluntary application Server Identification) as described in RFC8292.

For a brief understanding I take as a reference the use of the library web-push written in JavaScript to run in NodeJs... this library follows the Web push protocol as well as the RFC8030 for encrypt the messages of this API in addition to, contain a method to generate keys VAPID (useful because it saves labor).

Example (timeline):

1-generate a key pair VAPID from your server

2-Create "Subscription/remove subscription" routes (using routes Express.js)

3-Add a Service Worker

4-request the user to use notifications (and have approval)

5-Test (register/remove register)

Example code (generate VAPID Keys) - (See commented code gist):

const webpush = require('web-push')
const vapidKeys = webpush.generateVAPIDKeys()
console.log({
    pub: vapidKeys.publicKey,
    pri: vapidKeys.privateKey
})

Both keys (public/private) are used on the server already the public key is used on the front-end in the service Worker that we will see later.

Example code (creating routes) - (See commented Code gist):

const express = require('express')
const router = express.Router()
webpush.setVapidDetails(
    'mailto:[email protected]',
    process.env.PUSH_PUB,
    process.env.PUSH_PRI
)
router.post('push/subscribe', (req, res, next) => {
    const subscription = {
        endpoint: req.body.endpoint,
        keys: {
            p256dh: req.body.keys.p256dh,
            auth: req.body.keys.auth
        }
    }
    let pushId = subscription.keys.p256dh.replace(/-/g,'').replace(/=/g,'')
    let payload = JSON.stringify({
        title: 'Um titulo legal',
        body: 'Alguma coisa aqui...',
        icon: 'imagem.png'
    })
    let options = {
        TTL: 60
    }
    webpush.sendNotification(subscription, payload, options).then(response => {
        //...
    }).catch(e => {
        //...
    })
})

router.post('/push/unsubscribe', (req, res, next) => {
    let pushId = req.body.keys.p256dh.replace(/-/g,'').replace(/=/g,'')
    //...
})

There is no pattern of answers to the front-end this is at the discretion of the developer... good tips would be:

  • for registration the statuses:
    • 500 for a "generalized" error (not described)
    • 406 if the requirements are not acceptable
    • 201 for a successful record (Created)
  • to remove enrollment statuses:
    • 500 for a "generalized" error (not described)
    • 404 if you do not find (there is) a registered key (Not Found)
    • 406 if the requirements are not acceptable
    • 200 for a successful removal (OK)

If the system is used only for users registered in the application the status 403 (Forbidden) for access attempts by users not logged in session.

Example Service Worker (sw.js) - (See commented code gist):

const applicationServerPublicKey = 'BKXyMNOcPJMEfNnYWUrErN86WCacx4jdfepDR23x-cHkLP7TUj2cZ6Sp_UFRHFZYSfx7-Bk4XJHWcPgGi7DaASc';
function extend() {
    try {
        let hasOwnProperty = Object.prototype.hasOwnProperty,
            target = {}
        for (let i = 0; i < arguments.length; i++) {
            let source = arguments[i]
            for (let key in source) {
                 if ( hasOwnProperty.call(source, key) ) {
                     target[key] = source[key]
                 }
            }
        }
        return target
    } catch(e) {
        return {}
    }
}
function urlB64ToUint8Array(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;
}
self.addEventListener('push', function(event){
    console.log('[Service Worker] Push Received.');
    let defautOptions = {
        title: 'Site Name',
        body: 'Mensagem padrão',
        icon: './img/icons/icon.png',
        badge: './img/icons/icon-badage.png',
        //image: './img/push-banner-1600x1100.jpg',
        //vibrate: [200, 100, 200],
        //sound: './media/audio/incoming.m4a',
        dir: 'auto',
        tag: 'default',
        requireInteraction: false,
        renotify: false,
        silent: false
    }
    var data = event.data.text()
    try {
        let object = JSON.parse(data)
        if ( 'data' in object ) {
            //...
        }
        object.timestamp = new Date().getTime()
        data = object
    } catch(ex) {
        data = {};
        data = extend(data, defaultOptions)
    }
    event.waitUntil(self.registration.showNotification(data.title, data));
});

self.addEventListener('notificationclick', function(event) {
    console.log('[Service Worker] Notification click Received.');
    event.notification.close();
    if ( Object.keys(event.notification).length > 0 ) {
        if ( 'data' in event.notification ) {
            let data = event.notification.data;
        }
    }
});

Example code (index.html) - (See commented code gist):

<DOCTYPE html>
<html lang="en-US">
<head>
    <charset="UTF-8">
    <title>Web Pus Test</title>
    <link type="text/css" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css">
</head>
<body>
    <section class="subscription-details js-subscription-details is-invisible">
        <p>Uma vez que você inscreveu seu usuário, você enviará esta inscrição para o seu servidor para armazenar em um banco de dados para que, quando você desejar enviar uma mensagem, você pode pesquisar pela assinatura e enviar uma mensagem para ela.</p>
        <pre><code class="js-subscription-json"></code></pre>
    </section>
    <button disabled class="js-push-btn btn btn-info rounded-0">
        Enable Push Messaging
    </button>

    <script>
        'use strict';
        const applicationServerPublicKey = 'BKXyMNOcPJMEfNnYWUrErN86WCacx4jdfepDR23x-cHkLP7TUj2cZ6Sp_UFRHFZYSfx7-Bk4XJHWcPgGi7DaASc';
        const pushButton = document.querySelector('.js-push-btn');
        let isSubscribed = false;
        let swRegistration = null;
        function urlB64ToUint8Array(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 updateBtn() {
            if ( Notification.permission === 'denied' ) {
                pushButton.textContent = 'Push Messaging Blocked.';
                pushButton.disabled = true;
                showSubscriptionObject(null);
                return;
            }
            if ( isSubscribed ) {
                pushButton.textContent = 'Disable Push Messaging';
            } else {
                pushButton.textContent = 'Enable Push Messaging';
            }
            pushButton.disabled = false;
        }
        function showSubscriptionObject(subscription) {
            const subscriptionJson = document.querySelector('.js-subscription-json');
            if ( subscription ) {
                subscriptionJson.textContent = JSON.stringify(subscription, null, 4);
            }
        }
        function subscribeUser() {
            swRegistration.pushManager.subscribe({
                userVisibleOnly: true,
                applicationServerKey: urlB64ToUint8Array(applicationServerPublicKey)
            }).then(function(subscription) {
                fetch('/push/subscribe', {
                    method: 'post',
                    mode: 'cors',
                    referrerPolicy: 'origin',
                    credentials: 'include',
                    headers: {
                        'Accept': 'application/json',
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(subscription)
                }).then(function(response) {
                    return response;
                }).then(function(text) {
                    console.log('User is subscribed.');

                    showSubscriptionObject(subscription);
                    isSubscribed = true;
                    updateBtn();
                }).catch(function(error) {
                    console.log('Failed to subscribe the user: ', err);
                    updateBtn();
                });
            }).catch(function(err) {
                console.log('Failed to subscribe the user: ', err);
            });
        }
        function unsubscribeUser(){
            swRegistration.pushManager.getSubscription()
            .then(function(subscription) {
              if ( subscription ) {
                  fetch('/push/unsubscribe', {
                      method: "POST",
                      headers: {
                          'Content-Type': 'application/json'
                      },
                      body: JSON.stringify(subscription)
                  }).then(function(response) {
                      if ( response.ok ) {
                          return response.json();
                      }
                      throw new Error('Failed unsubscrible Push Notifications! Return server code: ' + response.status);
                  }).then(function(json) {
                      showSubscriptionObject(null);

                      console.log('User is unsubscribed.');

                      isSubscribed = false;
                      updateBtn();
                      subscription.unsubscribe();
                  }).catch(function(error) {
                      console.log('Error unsubscribing', error.message);
                  });
              }
          });
        }
        function initialiseUI() {
            pushButton.addEventListener('click', function() {
                pushButton.disabled = true;
                if ( isSubscribed ) {
                    unsubscribeUser();
                } else {
                    subscribeUser();
                }
            });
            swRegistration.pushManager.getSubscription()
            .then(function(subscription) {
                isSubscribed = !(subscription === null);
                showSubscriptionObject(subscription);

                if ( isSubscribed ) {
                    console.log('User IS subscribed.');
                } else {
                    console.log('User is NOT subscribed.');
                }
                updateBtn();
            });
        }
        if ( 'serviceWorker' in navigator && 'PushManager' in window ) {
            navigator.serviceWorker.register('./sw.js').then(function(swReg) {
                swRegistration = swReg;
                initialiseUI();
            }).catch(function(error){
                console.error('Service Worker Error', error);
            });
        } else {
            console.warn('Push messaging is not supported');
            pushButton.textContent = 'Push Not Supported';
        }
    </script>
</body>
</html>

It is not necessary to create a "Progressive Web App (PWA)" to test Web Push as well as it is not necessary to create a storage "offline-first"... all API's involved in this example (Notifications, Service Worker, Push) work in the localhost no need for a certificate.

By default browsers identify that the code is run locally and do not apply any type of enforcement normally applied on a "regular" domain (in production).

This is a basic example... the "data" property if added to the "payload" actually has great importance since it allows the developer to pass additional parameters to the notification.

The "codelab" from Google Developers remains the best bad starting point, the "cookbook" made by the same author (Matt Gaunt ) is the" Holy Grail " of implementation.


Sources and references (gist)


Footnotes :

1Push : in the scope of the question the Android system is defined however, for a better understanding and clarification: references to other systems were used.

2sppcd : Tizen reference in its documentation the use of a " push service (sppcd) ' : Tizen sppcd However, the closest I came in a search by the term "sppcd" was to the service "Samsung Push Service".

 4
Author: Lauro Moraes, 2017-12-27 19:41:06