|
|
@ -13,11 +13,12 @@ PROVIDER is FCM (Firebase Cloud Messaging) which is owned by Google. |
|
|
|
3) The Web Application that a user is visiting from their web browser. Let's |
|
|
|
call this the SERVICE (short for Web Push application service) |
|
|
|
[4) A Custom Web Push Intermediary Service, either third party or self-hosted. |
|
|
|
Called INTERMEDIARY here.] |
|
|
|
Called INTERMEDIARY here. FCM also may fit in this category if the SERVICE |
|
|
|
has an API key from FCM.] |
|
|
|
|
|
|
|
The workflow works like this: |
|
|
|
|
|
|
|
BROWSER visits a website which has a SERVICE. |
|
|
|
BROWSER visits a website which hosts a SERVICE. |
|
|
|
|
|
|
|
The SERVICE asks BROWSER for its permission to subscribe to messages coming |
|
|
|
from the SERVICE. |
|
|
@ -25,10 +26,10 @@ from the SERVICE. |
|
|
|
The SERVICE will provide context and obtain explicit permission before prompting |
|
|
|
for notification permission: |
|
|
|
|
|
|
|
In orer to provide this context and explict permission a two-step opt-in process |
|
|
|
In order to provide this context and explict permission a two-step opt-in process |
|
|
|
where the user is first presented with a pre-permission dialog box that explains |
|
|
|
what the notifications are for and why they are useful. This may help reduce the |
|
|
|
possibility of users clicking "don't allow. |
|
|
|
possibility of users clicking "don't allow". |
|
|
|
|
|
|
|
Now, to explain what happens in Typescript, we can activate a browser's |
|
|
|
permission dialogue in this manner: |
|
|
@ -52,8 +53,15 @@ function askPermission(): Promise<NotificationPermission> { |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
If the user grants permission, the client application registers a service worker |
|
|
|
using the `ServiceWorkerRegistration` API. |
|
|
|
The Notification.permission property indicates the permission level for the |
|
|
|
current session and returns one of the following string values: |
|
|
|
|
|
|
|
'granted': The user has granted permission for notifications. |
|
|
|
'denied': The user has denied permission for notifications. |
|
|
|
'default': The user has not made a choice yet. |
|
|
|
|
|
|
|
Once the user has granted permission, the client application registers a service |
|
|
|
worker using the `ServiceWorkerRegistration` API. |
|
|
|
|
|
|
|
The `ServiceWorkerRegistration` API is accessible via the browser's `navigator` |
|
|
|
object and the `navigator.serviceWorker` child object and ultimately directly |
|
|
@ -77,8 +85,8 @@ navigator.serviceWorker.register('sw.js', { scope: '/' }) |
|
|
|
``` |
|
|
|
|
|
|
|
The `sw.js` file contains the logic for what a service worker should do. |
|
|
|
It executes in a separate thread from the web page but provides a means |
|
|
|
of communicating between itself and the web page via messages. |
|
|
|
It executes in a separate thread of execution from the web page but provides a |
|
|
|
means of communicating between itself and the web page via messages. |
|
|
|
|
|
|
|
Note that there is a scope can specify what network requests it may |
|
|
|
intercept. |
|
|
@ -86,11 +94,11 @@ intercept. |
|
|
|
The Vue project already has its own service worker but it is possible to |
|
|
|
create multiple service worker files by registering them on different scopes. |
|
|
|
|
|
|
|
It is useful architecturally to specify a separate server worker. |
|
|
|
It is useful architecturally to specify a separate server worker file. |
|
|
|
|
|
|
|
In the case of web push, the path of the scope only has reference to the domain |
|
|
|
of the service worker and no relationship to the pathing for the web push |
|
|
|
server. In order to specify different server workers they need to be on |
|
|
|
server. In order to specify more than one server workers each needs to be on |
|
|
|
different scope paths! |
|
|
|
|
|
|
|
Here's a version which can be used for testing locally. Note there can be |
|
|
@ -126,22 +134,59 @@ module.exports = { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Once we have the service worker registered and the ServiceWorkerRegistration is |
|
|
|
returned, we then have access to a `pushManager` property object. This property |
|
|
|
allows us to continue with the web push work flow. |
|
|
|
|
|
|
|
In the next step, BROWSER requests a data structure from SERVICE called a VAPID |
|
|
|
(Voluntary Application Server Identification) which is the public key from a |
|
|
|
key-pair. |
|
|
|
|
|
|
|
The VAPID is a specification used to identify the application server (i.e. the |
|
|
|
SERVICE server) that is sending push messages through a push PROVIDER. It's an |
|
|
|
authentication mechanism that allows the server to demonstrate its identity to |
|
|
|
the push PROVIDER, by use of a public and private key pair. These keys are used |
|
|
|
by the SERVICE in encrypting messages being sent to the BROWSER, as well as |
|
|
|
being used by the BROWSER in decrypting the messages coming from the SERVICE. |
|
|
|
|
|
|
|
The VAPID (Voluntary Application Server Identification) key provides more |
|
|
|
security and authenticity for web push notifications in the following ways: |
|
|
|
|
|
|
|
Identifying the Application Server: |
|
|
|
|
|
|
|
The VAPID key is used to identify the application server that is sending |
|
|
|
the push notifications. This ensures that the push notifications are |
|
|
|
authentic and not sent by a malicious third party. |
|
|
|
|
|
|
|
Encrypting the Messages: |
|
|
|
|
|
|
|
The VAPID key is used to sign the push notifications sent by the |
|
|
|
application server, ensuring that they are not tampered with during |
|
|
|
transmission. This provides an additional layer of security and |
|
|
|
authenticity for the push notifications. |
|
|
|
|
|
|
|
In the next step, BROWSER requests a data structure from SERVICE called a VAPID (Voluntary |
|
|
|
Application Server Identification) which is the public key from a key-pair. |
|
|
|
Adding Contact Information: |
|
|
|
|
|
|
|
The VAPID is a specification used to identify the application server (i.e. the SERVICE |
|
|
|
server) that is sending push messages to a push service. It's an authentication |
|
|
|
mechanism that allows the server to demonstrate its identity to the push service, by use |
|
|
|
of a public and private key pair. These keys are used by the SERVICE in encrypting |
|
|
|
messages being sent to the BROWSER, as well as being used by the BROWSER in |
|
|
|
decrypting the messages coming from the SERVICE. |
|
|
|
The VAPID key allows a web application to add contact information to |
|
|
|
the push messages sent to the browser push service. This enables the |
|
|
|
push service to contact the application server in case of need or |
|
|
|
provide additional debug information about the push messages. |
|
|
|
|
|
|
|
Improving Delivery Rates: |
|
|
|
|
|
|
|
Using the VAPID key can help improve the overall performance of web push |
|
|
|
notifications, specifically improving delivery rates. By streamlining the |
|
|
|
delivery process, the chance of delivery errors along the way is lessened. |
|
|
|
|
|
|
|
If the BROWSER accepts and grants permission to subscribe to receiving from the |
|
|
|
SERVICE Web Push messages, then the BROWSER makes a subscription request to |
|
|
|
PROVIDER which creates and stores a special URL for that BROWSER. |
|
|
|
|
|
|
|
const applicationServerKey = urlBase64ToUint8Array('BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'); |
|
|
|
Here's a bit of code describing the above process: |
|
|
|
|
|
|
|
// b64 is the VAPID |
|
|
|
b64 = 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'; |
|
|
|
const applicationServerKey = urlBase64ToUint8Array(b64); |
|
|
|
const options: PushSubscriptionOptions = { |
|
|
|
userVisibleOnly: true, |
|
|
|
applicationServerKey: applicationServerKey |
|
|
@ -155,68 +200,111 @@ registration.pushManager.subscribe(options) |
|
|
|
console.error('Push subscription failed:', error); |
|
|
|
}); |
|
|
|
|
|
|
|
In this example, the `applicationServerKey` variable contains the VAPID public key, |
|
|
|
which is converted to a Uint8Array using the `urlBase64ToUint8Array()` function from the |
|
|
|
convert-vapid-public-key package. The options object is of type PushSubscriptionOptions, |
|
|
|
which includes the `userVisibleOnly` and `applicationServerKey` (ie VAPID public key) |
|
|
|
properties. The subscribe() method returns a `Promise` that resolves to a `PushSubscription` |
|
|
|
object containing details of the subscription, such as the endpoint URL and the public key. |
|
|
|
In this example, the `applicationServerKey` variable contains the VAPID public |
|
|
|
key, which is converted to a `Uint8Array` using a function such as this: |
|
|
|
|
|
|
|
The VAPID (Voluntary Application Server Identification) key provides more security and |
|
|
|
authenticity for web push notifications in the following ways: |
|
|
|
``` |
|
|
|
export function toUint8Array(base64String: string, atobFn: typeof atob): Uint8Array { |
|
|
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4); |
|
|
|
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); |
|
|
|
|
|
|
|
Identifying the Application Server: |
|
|
|
const rawData = atobFn(base64); |
|
|
|
const outputArray = new Uint8Array(rawData.length); |
|
|
|
|
|
|
|
The VAPID key is used to identify the application server that is sending the push notifications. |
|
|
|
This ensures that the push notifications are authentic and not sent by a malicious third party. |
|
|
|
for (let i = 0; i < rawData.length; ++i) { |
|
|
|
outputArray[i] = rawData.charCodeAt(i); |
|
|
|
} |
|
|
|
return outputArray; |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
Encrypting the Messages: |
|
|
|
The options object is of type `PushSubscriptionOptions`, which includes the |
|
|
|
`userVisibleOnly` and `applicationServerKey` (ie VAPID public key) properties. |
|
|
|
|
|
|
|
The VAPID key is used to sign the push notifications sent by the application server, |
|
|
|
ensuring that they are not tampered with during transmission. This provides an additional |
|
|
|
layer of security and authenticity for the push notifications. |
|
|
|
The subscribe() method returns a `Promise` that resolves to a `PushSubscription` |
|
|
|
object containing details of the subscription, such as the endpoint URL and the |
|
|
|
public key. The returned data would have a form like this: |
|
|
|
|
|
|
|
Adding Contact Information: |
|
|
|
{ |
|
|
|
"endpoint": "https://some.pushservice.com/some/unique/identifier", |
|
|
|
"expirationTime": null, |
|
|
|
"keys": { |
|
|
|
"p256dh": "some_base64_encoded_string", |
|
|
|
"auth": "some_other_base64_encoded_string" |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
The VAPID key allows a web application to add contact information to the push messages sent to the browser push service. |
|
|
|
This enables the push service to contact the application server in case of need or provide additional debug information about the push messages. |
|
|
|
endpoint: A string representing the endpoint URL for the push service. This |
|
|
|
URL is essentially the push service address to which the push message would |
|
|
|
be sent for this particular subscription. |
|
|
|
|
|
|
|
Improving Delivery Rates: |
|
|
|
expirationTime: A DOMHighResTimeStamp (which is basically a number or null) |
|
|
|
representing the subscription's expiration time in milliseconds since |
|
|
|
01 January, 1970 UTC. This can be null if the subscription never expires. |
|
|
|
|
|
|
|
options: An object that contains the options used for creating the |
|
|
|
subscription. This object itself has the following sub-properties: |
|
|
|
|
|
|
|
Using the VAPID key can help improve the overall performance of web push notifications, specifically improving delivery rates. |
|
|
|
By streamlining the delivery process, the chance of delivery errors along the way is lessened. |
|
|
|
applicationServerKey: A public key your push service uses for application |
|
|
|
server identification. This is normally a Uint8Array. |
|
|
|
|
|
|
|
userVisibleOnly: A boolean value indicating that the push messages that |
|
|
|
are sent should be made visible to the user through a notification. |
|
|
|
This is often set to true. |
|
|
|
|
|
|
|
The PROVIDER sends this URL back to the BROWSER. The BROWSER will then use that |
|
|
|
URL to check for incoming messages by way of a special software named a "service |
|
|
|
worker". The BROWSER also sends this URL back to SERVICE which will use that |
|
|
|
URL to send messages to the BROWSER via the PROVIDER. |
|
|
|
The BROWSER will, internally, then use that URL to check for incoming messages |
|
|
|
by way of the service worker we described earlier. The BROWSER also sends this |
|
|
|
URL back to SERVICE which will use that URL to send messages to the BROWSER via |
|
|
|
the PROVIDER. |
|
|
|
|
|
|
|
Ultimately, the actual process of receiving messages varies from BROWSER to |
|
|
|
BROWSER. Approaches vary from long-polling HTTP connections to WebSockets. A |
|
|
|
Ultimately, the actual internal process of receiving messages varies from BROWSER |
|
|
|
to BROWSER. Approaches vary from long-polling HTTP connections to WebSockets. A |
|
|
|
lot of handwaving and voodoo magic. The bottom line is that the BROWSER itself |
|
|
|
manages the connection to the PROVIDER whilst the SERVICE must send messages |
|
|
|
via the PROVIDER so that they reach the BROWSER. |
|
|
|
via the PROVIDER so that they reach the BROWSER service worker. |
|
|
|
|
|
|
|
Just to remind us that in our service worker our code for receiving messages |
|
|
|
will look something like this: |
|
|
|
|
|
|
|
self.addEventListener('push', function(event: PushEvent) { |
|
|
|
console.log('Received a push message', event); |
|
|
|
|
|
|
|
const title = 'Push message'; |
|
|
|
const body = 'The message body'; |
|
|
|
const icon = '/images/icon-192x192.png'; |
|
|
|
const tag = 'simple-push-demo-notification-tag'; |
|
|
|
|
|
|
|
event.waitUntil( |
|
|
|
self.registration.showNotification(title, { |
|
|
|
body: body, |
|
|
|
icon: icon, |
|
|
|
tag: tag |
|
|
|
}) |
|
|
|
); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Now to address the issue of receiving notification messages on mobile devices. |
|
|
|
It should be noted that Web Push messages are only received when BROWSER is |
|
|
|
open, except in the cases of Chrome and Firefox mobile BROWSERS. In iOS the |
|
|
|
open, except in the cases of Chrome and Firefox mobile BROWSERS. In iOS, the |
|
|
|
mobile application (in our case a PWA) must be added to the Home Screen and |
|
|
|
permissions must be explicitly granted that allow the application to receive push |
|
|
|
notifications. Further, with an iOS device the user must enable wake on notification to |
|
|
|
have their device light-up when it receives a notification (https://support.apple.com/enus/HT208081). |
|
|
|
permissions must be explicitly granted that allow the application to receive |
|
|
|
push notifications. Further, with an iOS device the user must enable wake on |
|
|
|
notification to have their device light-up when it receives a notification |
|
|
|
(https://support.apple.com/enus/HT208081). |
|
|
|
|
|
|
|
So what about #4? - The INTERMEDIARY. Well, It is possible under very special |
|
|
|
circumstances to create your own Web Push PROVIDER. The only case I've found so |
|
|
|
far relates to making an Android Custom ROM. (An Android Custom ROM is a |
|
|
|
customized version of the Android Operating System.) There are open source |
|
|
|
IMTERMEDIARY products such as UnifiedPush (https://unifiedpush.org/) which can |
|
|
|
fulfill this role. If you are using iOS you are not permitted to make or use your own |
|
|
|
custom Web Push PROVIDER. Apple will never allow anyone to do that. Apple has |
|
|
|
none of its own. |
|
|
|
fulfill this role. If you are using iOS you are not permitted to make or use |
|
|
|
your own custom Web Push PROVIDER. Apple will never allow anyone to do that. |
|
|
|
Apple has none of its own. |
|
|
|
|
|
|
|
It is, however, possible to have a sort of proxy working between your SERVICE and |
|
|
|
FCM (or iOS). Services that mash up various Push notification services (like |
|
|
|
It is, however, possible to have a sort of proxy working between your SERVICE |
|
|
|
and FCM (or iOS). Services that mash up various Push notification services (like |
|
|
|
OneSignal) can perform in the role of such proxies. |
|
|
|
|
|
|
|
#4 -The INTERMEDIARY- doesn't appear to be anything we should be spending our |
|
|
|