pusher / pusher-http-php

PHP library for interacting with the Pusher Channels HTTP API
https://pusher.com/docs/server_api_guide
1.41k stars 309 forks source link

Pusher Beams Enquiry #369

Open Ifriqiya opened 1 year ago

Ifriqiya commented 1 year ago

I need help with a web push notifications setup that works except that it only relays the message body.

I have this job that sends a push:

public function handle() 
    {
        $body = 'You have a new chat message from '.$this->message->user->first_name.'!';
        $id = $this->message->user->id;
        $beamsClient = new PushNotifications(
            [
                'instanceId' => config('services.beams_instance_id'),
                'secretKey' => config('services.beams_secret_key'),
            ]
        );

        $beamsClient->publishToUsers([strval($this->message->recipient->id)], 
            [
                'web' => [
                    'notification' => [
                        'body' => $body, 
                        'icon' => config('app.url').'/img/favicon-16x16.png', 
                        'deep_link' => config('app.url').'/chat/messages/'.$this->message->recipient->id
                    ],
                    'data' => ['id' => $id]
                ]                
            ]
        );
    }

I get the push but only the message body is sent. I get this dev console:

{notification: {…}, data: {…}, topic: 'cbbb7d3ffcc5fcf960dd3c6c12e2c63f'}
data
: 
{pusher: {…}}
notification
: 
body
: 
"You have a new chat message from Augustina!"
[[Prototype]]
: 
Object
topic
: 
"cbbb7d3ffcc5fcf960dd3c6c12e2c63f"
[[Prototype]]
: 
Object

How do I resolve this?

benw-pusher commented 1 year ago

Are you looking to add a title to your notification? Or are you having issues with the icon/deep_link?

Ifriqiya commented 1 year ago

Hi @benw-pusher, I'm having issues with the icon/deep_link.

benw-pusher commented 1 year ago

These options should be handled by the client, what library are you using and how does the notification display there?

Ifriqiya commented 1 year ago

I'm using "@pusher/push-notifications-web". The notification shows up on the browser.

benjamin-tang-pusher commented 1 year ago

Hi, can you hard code your deeplink to a test value like "https://www.google.com". If you then click the notification, does it take you there (or show in your dev console)?

Ifriqiya commented 1 year ago

Hi, thanks for your response. I tried your suggestion but I still don't get navigated to the hard coded url. I get the correct url in dev console in both cases-with $this->url and the hard coded value. It just doesn't navigate there.

benjamin-tang-pusher commented 1 year ago

There may be an issue with the receiving browser. Which browser are you using?

Ifriqiya commented 1 year ago

I primarily use Brave, but I have tried with Edge and Mozilla.

benjamin-tang-pusher commented 1 year ago

Could you try another machine, just in the off chance there is machine issue stopping the notification opening the link in the browser?

benw-pusher commented 1 year ago

Could you share the code used in the client for subscribing to the interest and receiving the notification?

Ifriqiya commented 1 year ago

This is it:

//
import * as PusherPushNotifications from "@pusher/push-notifications-web"
//
let globalBeamsClient
//
onMounted(() => {
//

  window.navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
    const userId = user.value.id
    const currentUserId = userId.toString()
    const beamsClient = new PusherPushNotifications.Client({
      instanceId: import.meta.env.VITE_PUSHER_BEAMS_INSTANCE_ID,
      serviceWorkerRegistration: serviceWorkerRegistration
    })
    globalBeamsClient = beamsClient
    const beamsTokenProvider = new PusherPushNotifications.TokenProvider({
      url: route("pusher.beams-auth")
    })
    beamsClient
      .start()
      .then((beamsClient) => beamsClient.setUserId(currentUserId, beamsTokenProvider))
      .catch(console.error)
  })

service worker code:

// This is the "Offline copy of assets" service worker
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js')

const {BackgroundSyncPlugin} =  workbox.backgroundSync
const {registerRoute} =  workbox.routing
const {StaleWhileRevalidate} =  workbox.strategies
const {ExpirationPlugin} =  workbox.expiration

const CACHE = "offlineAssets"
const QUEUE_NAME = "bgSyncQueue"

self.__WB_DISABLE_DEV_LOGS = true

self.addEventListener('install', () => self.skipWaiting())

self.addEventListener('activate', () => self.clients.claim())

const bgSyncPlugin = new BackgroundSyncPlugin(QUEUE_NAME, {
  maxRetentionTime: 24 * 60 // Retry for max of 24 Hours (specified in minutes)
})

const expPlugin = new ExpirationPlugin({
  maxEntries: 5,
  maxAgeSeconds: 1 * 24 * 60 * 60,
  purgeOnQuotaError: true,
  matchOptions: {
    ignoreVary: true,
  }
})

registerRoute(
  new RegExp('/*'),
  new StaleWhileRevalidate({
    cacheName: CACHE,
    plugins: [
      bgSyncPlugin,
      expPlugin
    ]
  })
)

self.addEventListener('push', (e) => {
  if (!(self.Notification && self.Notification.permission === 'granted')) return
  if (!e.data) return

  const msg = e.data.json()

  e.waitUntil(clients.matchAll()
    .then((clients) => {
    // console.log(msg, clients)
    clients.forEach((client) => {
      const url = client.url
      const id = url.slice(url.lastIndexOf('/') + 1)
      const clientId = client.id
      if (!client.url.includes(`chat/messages/${id}`)) { //send only if not on chat url and if clientId matches client id of sender.
        self.registration.showNotification('SmartWealth Push Notification', {
          body: msg.notification.body,
          icon: msg.notification.icon,
          actions: msg.notification.actions,
          deep_link: msg.notification.deep_link
        })
      }
    })
  }))
})

self.addEventListener('notificationclick', () => {}) //to do
benw-pusher commented 1 year ago

Your service worker doesn't appear to have the Beams service worker defined - you need to add this as per https://pusher.com/docs/beams/guides/existing-service-worker/#import-the-beams-service-worker-in-your-existing-service-worker-file.

I can also see you have provided some custom code for handling the receipt and display of the notification - however this differed from the documented approach for this - https://pusher.com/docs/beams/guides/handle-incoming-notifications/web/#overriding-default-sdk-behavior.

I am able to access the deep_link as expected with the following payload:

web: {
            notification: {
                title: 'hi!!',
                body: 'hi. 😃👍🎉!',
                deep_link: 'https://bbc.co.uk',
                icon: 'http://localhost:9000/img/favicon-16x16.png'

            }
        },

and the following service worker:

importScripts("https://js.pusher.com/beams/service-worker.js");

PusherPushNotifications.onNotificationReceived = ({
    payload,
    pushEvent,
    handleNotification,
}) => {

  const promiseChain = isClientFocused().then((clientIsFocused) => {
    if (clientIsFocused) {
      self.console.log(payload);
      console.log("Don't need to show a notification.");
      return;
    }

    // Client isn't focused, we need to show a notification.
    self.console.log(payload);
    console.log("had to show a notification.");
    return self.registration.showNotification('Had to show a notification.');
  });

  pushEvent.waitUntil(promiseChain);

           /*pushEvent.waitUntil(
           self.registration.showNotification(payload.notification.title, {
              body: payload.notification.body,
              icon: payload.notification.icon,
              data: payload.data, 
            }) 
          )};*/
          };

          function isClientFocused() {
            return clients
              .matchAll({
                type: 'window',
                includeUncontrolled: true,
              })
              .then((windowClients) => {
                let clientIsFocused = false;

                for (let i = 0; i < windowClients.length; i++) {
                  const windowClient = windowClients[i];
                  if (windowClient.focused) {
                    clientIsFocused = true;
                    break;
                  }
                }

                return clientIsFocused;
              });
          }

Here I am using the payload parameter in the PusherPushNotification.onNotificationReceived handler: if you were to move to using the onNotificationRecived handler with this payload instead of the addEventListener and const msg = e.data.json() you should see some results.

Ifriqiya commented 1 year ago

OK, thanks. I'll try it out.

Ifriqiya commented 1 year ago

Hi @benw-pusher I wanted to test out your suggestion but I can't get past beams authentication which previously worked. What could be wrong here:

//this my method
    public function beamsAuth(Request $request, User $user)
    {
        $userID = strval($user->id);
        $userIDInQueryParam = $request->input('user_id');

        $beamsClient = new PushNotifications(
            [
                'instanceId' => config('services.pusher.beams_instance_id'),
                'secretKey' => config('services.pusher.beams_secret_key'),
            ]
        );

        if ($userID != $userIDInQueryParam) {
            return response('Inconsistent request', 401);
        } else {
            $beamsToken = $beamsClient->generateToken($userID);

            return response()->json($beamsToken);
        }
    }
//my route
    Route::get('pusher/beams-auth', [NotificationsController::class, 'beamsAuth'])->name('pusher.beams-auth');
//section of front-end where I attempt authorisation 
  window.navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
    const userId = user.value.id
    // const currentUserId = userId.toString()
    const beamsClient = new PusherPushNotifications.Client({
      instanceId: import.meta.env.VITE_PUSHER_BEAMS_INSTANCE_ID,
      serviceWorkerRegistration: serviceWorkerRegistration
    })
    globalBeamsClient = beamsClient
    const beamsTokenProvider = new PusherPushNotifications.TokenProvider({
      url: route("pusher.beams-auth"),
      queryParams: {
        user_id: userId,
      },
    })
    beamsClient
      .start()
      .then((beamsClient) => beamsClient.setUserId(userId, beamsTokenProvider))
      .catch(console.error)
  })
//console errors
@pusher_push-notifications-web.js?v=2e7db2b7:604     GET http://127.0.0.1:8000/pusher/beams-auth?user_id=3446ydfm66 
 401 (Unauthorized)
Error: Unexpected status code 401: Cannot parse error response
    at _callee2$ (@pusher_push-notifications-web.js?v=2e7db2b7:670:21)
    at tryCatch (@pusher_push-notifications-web.js?v=2e7db2b7:48:42)
    at Generator.invoke [as _invoke] (@pusher_push-notifications-web.js?v=2e7db2b7:203:24)
    at prototype.<computed> [as throw] (@pusher_push-notifications-web.js?v=2e7db2b7:80:23)
    at asyncGeneratorStep (@pusher_push-notifications-web.js?v=2e7db2b7:509:24)
    at _throw (@pusher_push-notifications-web.js?v=2e7db2b7:530:9)

//beams storage with token but null user_id
<html>
<body>
<!--StartFragment-->

"xxx" | {instance_id: 'xxx', device_id: 'webxxx', token: 'eyJlbmRwb2ludCI6Imh0dHBzOi8vZmNtLmdvb2dsZWFwaXMuY2…ZIiwiYXV0aCI6Imp3OGJ0eEFnWTVoSXlBdXpUUFBhLWcifX0=', user_id: null, last_seen_sdk_version: '1.1.0', …}device_id: "web-xxx"instance_id: "xxx"last_seen_sdk_version: "1.1.0"last_seen_user_agent: "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36"token: "eyJlbmRwb2ludCI6Imh0dHBzOi8vZmNtLmdvb2dsZWFwaXMuY29tL2ZjbS9zZW5kL2RPM2RpZGlLTWp3OkFQQTkxYkZmMHRHWjFzRTI4Y1BHR3J4ZnA0c3EtSFdRZDRPX1RDOVkxdzNSYWxzXzVwQTZTbno4VTF1dzhaQkwzOWR0V2VOcHNqZ242bmNxdk1oMGEzeE9PTG1VXzFSanFlc2JsYkVOQUNRM3FrdWpUamo5WV84TnNQZkUxWllTMEtqaU84YWd4SmNGIiwiZXhwaXJhdGlvblRpbWUiOm51bGwsImtleXMiOnsicDI1NmRoIjoiQkNEekpQZ3dTbVVnd3ZyYnJudW9nQXlRT0xsZ2YteUxtMUdvN2Zxd0t4bFdRZVEtZEo5WEdwWlItRF9Jd2hOcC1Bd0tkXy1iVTVQMnU2YlRQWl9hTnlZIiwiYXV0aCI6Imp3OGJ0eEFnWTVoSXlBdXpUUFBhLWcifX0="user_id: null
-- | --

<!--EndFragment-->
</body>
</html>

What could be the issue?

benw-pusher commented 1 year ago

The error suggests your auth endpoint is returning 401. Have you verified that the auth is passing the check if ($userID != $userIDInQueryParam) {?

Ifriqiya commented 12 months ago

Hi @benw-pusher, thanks for your time on this matter. Implementing your approach produced this error when I click on the notification service-worker.js:1421 Uncaught TypeError: Cannot read properties of null (reading 'pusher') at service-worker.js:1421:36 at this point which seems to need a 'pusher' variable:

self.addEventListener('notificationclick', function (e) {
  var pusher = e.notification.data.pusher;
  var isPusherNotification = pusher !== undefined;

  if (isPusherNotification) {
    // Report analytics event, best effort
    self.PusherPushNotifications.reportEvent({
      eventType: 'open',
      pusherMetadata: pusher.pusherMetadata
    });

    if (pusher.customerPayload.notification.deep_link) {
      e.waitUntil(clients.openWindow(pusher.customerPayload.notification.deep_link));
    }

    e.notification.close();
  }
});

It appears that the 'optons' variable requirement was not provided:

           case 8:
              title = payloadFromCallback.notification.title || '';
              body = payloadFromCallback.notification.body || '';
              icon = payloadFromCallback.notification.icon;
              options = {
                body: body,
                icon: icon,
                data: {
                  pusher: {
                    customerPayload: payloadFromCallback,
                    pusherMetadata: pusherMetadata
                  }
                }
              };

This is my current SW code:

// This is the "Offline copy of assets" service worker
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js')
importScripts("https://js.pusher.com/beams/service-worker.js")

const {BackgroundSyncPlugin} =  workbox.backgroundSync
const {registerRoute} =  workbox.routing
const {StaleWhileRevalidate} =  workbox.strategies
const {ExpirationPlugin} =  workbox.expiration

const CACHE = "offlineAssets"
const QUEUE_NAME = "bgSyncQueue"

self.__WB_DISABLE_DEV_LOGS = true

self.addEventListener('install', () => self.skipWaiting())

self.addEventListener('activate', () => self.clients.claim())

const bgSyncPlugin = new BackgroundSyncPlugin(QUEUE_NAME, {
  maxRetentionTime: 24 * 60 // Retry for max of 24 Hours (specified in minutes)
})

const expPlugin = new ExpirationPlugin({
  maxEntries: 5,
  maxAgeSeconds: 1 * 24 * 60 * 60,
  purgeOnQuotaError: true,
  matchOptions: {
    ignoreVary: true,
  }
})

registerRoute(
  new RegExp('/*'),
  new StaleWhileRevalidate({
    cacheName: CACHE,
    plugins: [
      bgSyncPlugin,
      expPlugin
    ]
  })
)

PusherPushNotifications.onNotificationReceived = ({ payload, pushEvent, handleNotification, }) => {

  const promiseChain = isClientFocused().then((clientIsFocused) => {
    if (clientIsFocused) {
      self.console.log(payload);
      console.log("Don't need to show a notification.");
      return
    }

    // Client isn't focused, we need to show a notification.
    self.console.log(payload);
    console.log("had to show a notification.");
    return self.registration.showNotification(payload.notification.title, {
      body: payload.notification.body,
      icon: payload.notification.icon,
      // actions: payload.notification.actions,
      deep_link: payload.notification.deep_link
    })
  })

  pushEvent.waitUntil(promiseChain)
}

function isClientFocused() {
  return clients
    .matchAll({
      type: 'window',
      includeUncontrolled: true,
    })
    .then((windowClients) => {
      let clientIsFocused = false

      for (let i = 0; i < windowClients.length; i++) {
        const windowClient = windowClients[i]
        if (windowClient.focused) {
          clientIsFocused = true
          break
        }
      }

      return clientIsFocused
    })
}

What could I have missed or done wrong? Do I need to add some pusher related data to my payload? I also noticed that handleNotification is defined but not used, could it be a reason, what is this meant to do?

benw-pusher commented 11 months ago

I believe I previously shared a slightly erroneous Service Worker with you that included some other testing I was conducting at the time. You can remove the following from the SW:

PusherPushNotifications.onNotificationReceived = ({ payload, pushEvent, handleNotification, }) => {

  const promiseChain = isClientFocused().then((clientIsFocused) => {
    if (clientIsFocused) {
      self.console.log(payload);
      console.log("Don't need to show a notification.");
      return
    }

    // Client isn't focused, we need to show a notification.
    self.console.log(payload);
    console.log("had to show a notification.");
    return self.registration.showNotification(payload.notification.title, {
      body: payload.notification.body,
      icon: payload.notification.icon,
      // actions: payload.notification.actions,
      deep_link: payload.notification.deep_link
    })
  })

  pushEvent.waitUntil(promiseChain)
}

function isClientFocused() {
  return clients
    .matchAll({
      type: 'window',
      includeUncontrolled: true,
    })
    .then((windowClients) => {
      let clientIsFocused = false

      for (let i = 0; i < windowClients.length; i++) {
        const windowClient = windowClients[i]
        if (windowClient.focused) {
          clientIsFocused = true
          break
        }
      }

      return clientIsFocused
    })
Ifriqiya commented 11 months ago

If I remove what you are now suggesting from the SW that means there's no longer a push notifications feature. My original issue is the the deep link feature wasn't working. Are you now saying I should import the pusher service worker file and retain my previous push notifications feature code, and that this would sort the deep link issue? See your original suggestion below:

Your service worker doesn't appear to have the Beams service worker defined - you need to add this as per https://pusher.com/docs/beams/guides/existing-service-worker/#import-the-beams-service-worker-in-your-existing-service-worker-file.

I can also see you have provided some custom code for handling the receipt and display of the notification - however this differed from the documented approach for this - https://pusher.com/docs/beams/guides/handle-incoming-notifications/web/#overriding-default-sdk-behavior.

I am able to access the deep_link as expected with the following payload:

web: {
            notification: {
                title: 'hi!!',
                body: 'hi. 😃👍🎉!',
                deep_link: 'https://bbc.co.uk',
                icon: 'http://localhost:9000/img/favicon-16x16.png'

            }
        },

and the following service worker:

importScripts("https://js.pusher.com/beams/service-worker.js");

PusherPushNotifications.onNotificationReceived = ({
    payload,
    pushEvent,
    handleNotification,
}) => {

  const promiseChain = isClientFocused().then((clientIsFocused) => {
    if (clientIsFocused) {
      self.console.log(payload);
      console.log("Don't need to show a notification.");
      return;
    }

    // Client isn't focused, we need to show a notification.
    self.console.log(payload);
    console.log("had to show a notification.");
    return self.registration.showNotification('Had to show a notification.');
  });

  pushEvent.waitUntil(promiseChain);

           /*pushEvent.waitUntil(
           self.registration.showNotification(payload.notification.title, {
              body: payload.notification.body,
              icon: payload.notification.icon,
              data: payload.data, 
            }) 
          )};*/
          };

          function isClientFocused() {
            return clients
              .matchAll({
                type: 'window',
                includeUncontrolled: true,
              })
              .then((windowClients) => {
                let clientIsFocused = false;

                for (let i = 0; i < windowClients.length; i++) {
                  const windowClient = windowClients[i];
                  if (windowClient.focused) {
                    clientIsFocused = true;
                    break;
                  }
                }

                return clientIsFocused;
              });
          }

Here I am using the payload parameter in the PusherPushNotification.onNotificationReceived handler: if you were to move to using the onNotificationRecived handler with this payload instead of the addEventListener and const msg = e.data.json() you should see some results.

Ifriqiya commented 11 months ago

Testing this further, I updated my SW push feature to just this:

PusherPushNotifications.onNotificationReceived = ({ payload, pushEvent }) => {

   pushEvent.waitUntil(self.registration.showNotification(payload.notification.title, {
    body: payload.notification.body,
    icon: payload.notification.icon,
    data: payload.data,
    deep_link: payload.notification.deep_link
  }))
}

I add this segment that I didn't previously have data: payload.data, and I no longer have the error I mentioned earlier about a need for a 'pusher' variable, but I still wasn't navigated to the url in my notification data, meaning the deep_link feature still isn't working. My understanding is that this is part of the pusher SW import, is that correct or do I need to populate that variable?

benw-pusher commented 6 months ago

Does the deep_link show correctly if you log it out?

The format of your onNotificationReceived is still different to the documented mechanism at https://pusher.com/docs/beams/guides/handle-incoming-notifications/web/#adding-additional-custom-logic,-keeping-default-behavior

Ifriqiya commented 6 months ago

Does the deep_link show correctly if you log it out?

The format of your onNotificationReceived is still different to the documented mechanism at https://pusher.com/docs/beams/guides/handle-incoming-notifications/web/#adding-additional-custom-logic,-keeping-default-behavior

Yes it does. How so? Looks the same to me.

benw-pusher commented 6 months ago

You are calling the showNotification method and not the handleNotification method that is documented.

Ifriqiya commented 6 months ago

You are calling the showNotification method and not the handleNotification method that is documented.

Doesn't this https://pusher.com/docs/beams/guides/handle-incoming-notifications/web/#overriding-default-sdk-behavior use showNotification