noticed icon indicating copy to clipboard operation
noticed copied to clipboard

WebPush Delivery Method

Open excid3 opened this issue 3 years ago • 4 comments

Web Push requires a service worker and a model to work.

https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/

The Webpush gem provides some helpful features for generating VAPID keys, etc.

Some sample code:

class DeliveryMethods::Webpush < Noticed::DeliveryMethods::Base
  def deliver
    recipient.webpush_subscriptions.each do |sub|
      sub.publish(params[:message])
    end
  end
end
# == Schema Information
#
# Table name: webpush_subscriptions
#
#  id         :bigint           not null, primary key
#  endpoint   :string
#  auth_key   :string
#  p256dh_key :string
#  created_at :datetime         not null
#  updated_at :datetime         not null
#  user_id    :bigint           not null
#
class WebpushSubscription < ApplicationRecord
  belongs_to :user

  def publish(message)
    Webpush.payload_send(
      message: message,
      endpoint: endpoint,
      p256dh: p256dh_key,
      auth: auth_key,
      vapid: {
        private_key: Rails.application.credentials.dig(:webpush, :private_key),
        public_key: Rails.application.credentials.dig(:webpush, :public_key)
      }
    )
  end
end
class WebpushSubscriptionsController < ApplicationController
  skip_before_action :verify_authenticity_token

  def index
  end

  def create
    notification = WebpushNotification.find_by(auth_key: params[:keys][:auth])
    if !notification
      notification = WebpushNotification.new(
        user: current_user,
        endpoint: params[:endpoint],
        auth_key: params[:keys][:auth],
        p256dh_key: params[:keys][:p256dh],
      )
    end

    if notification.save
      render json: notification
    else
      render json: notification.errors.full_messages
    end
  end
end
   const vapidPublicKey = new Uint8Array(<%= Base64.urlsafe_decode64(Rails.application.credentials.dig(:webpush, :public_key)).bytes %>);

  // application.js
  // Register the serviceWorker script at /serviceworker.js from your server if supported
  if (navigator.serviceWorker) {
    navigator.serviceWorker.register('/service_worker.js')
      .then(function(reg) {
        navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
          serviceWorkerRegistration.pushManager
          .subscribe({
            userVisibleOnly: true,
            applicationServerKey: vapidPublicKey
          }).then(async function(sub) {
            const data = await fetch('/webpush_subscriptions', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
              },
              body: JSON.stringify(sub),
            }).then(r => r.json());
            console.log(data);
          });
        });
      });
  } else {
    console.error('Service worker is not supported in this browser');
  }

excid3 avatar Apr 23 '21 19:04 excid3

@excid3 push endpoints expire after a while.

Webpush gem method has a method to check for expired endpoints. I use that to clean up the database

Not sure if it can be part of this gem but might be worth considering

uurcank avatar Apr 23 '21 20:04 uurcank

something like this

rescue Webpush::Unauthorized # skip for now rescue Webpush::ExpiredSubscription sub.destroy end

which can rescue publish method

uurcank avatar Apr 23 '21 20:04 uurcank

Good to know about the expirations. 👍 I haven't tried this out before, so lots to learn.

excid3 avatar Apr 23 '21 21:04 excid3

@excid3 service worker has an event called 'pushsubscriptionchange' when the push endpoint expires, the event gets triggered which can be used to create another push endpoint in the background.

something like this

// Update push subscription if any change is detected
function onPushSubcriptionChange(event) {
  event.waitUntil(swRegistration.pushManager.subscribe(event.oldSubscription.options)
    .then(subscription => {

      const data = {
        subscription: subscription,
       };

      return fetch("/notifications/create", {
        method: "post",
        headers: {
          "Content-type": "application/json"
        },
            body: JSON.stringify(data),
      });
    })
  );

}

self.addEventListener('pushsubscriptionchange', onPushSubcriptionChange);

I am curious how you will choose to handle that. I could not see a way to update an existing subscription record so using those rescue methods to clean up the database and create a new subscription. There is not an identifier to use I think

there is also this idea to include service worker as part of asset pipeline or webpacker

uurcank avatar Apr 27 '21 14:04 uurcank

Not sure if this is still being considered for Noticed but big news today for WebPush. I've never bothered looking into WebPush until iOS/iPadOS devices supported it but it seems like WebPush will be becoming much more popular now

Tonksthebear avatar Feb 16 '23 22:02 Tonksthebear

I created a video walking through setting this up with full support for iOS clients even. Was wondering if there would be interest in getting this pulled in as a PR. My big question would be around what the javascript resources would look like since so much of that is going to be custom.

Video Link: https://youtu.be/WHZ4strj6_U Backing repo: https://github.com/jbennett/web_push_notifications

jbennett avatar Jul 05 '23 17:07 jbennett

I added a PR to automated most of the WebPush setup: #297

jbennett avatar Jul 06 '23 14:07 jbennett

Thanks @jbennett can't wait to check this out!

excid3 avatar Jul 08 '23 01:07 excid3