pusher / push-notifications-web

Beams Browser notifications
MIT License
39 stars 19 forks source link

Safari Push Notifications not enabled by user gesture #105

Closed heymartinadams closed 2 years ago

heymartinadams commented 2 years ago

Issue

In my React app, Iā€™m triggering push notifications via a user gesture onClick in Safari, but I still receive the following error:

Unhandled Promise Rejection: Error: Push notification prompting can only be done from a user gesture.
CleanShot 2022-05-12 at 16 20 01@2x

What am I doing wrong?

Replication

1. User Interface

// `token` and `userId` provided by the app
<button onClick={() => enablePushFn({ token, userId })}>Enable Notifications</button>
CleanShot 2022-05-12 at 15 48 47@2x

2. Function

const enablePushFn = ({ token, userId }) => {
  const beamsClient = new PusherPushNotifications.Client({
    instanceId: process.env.NEXT_PUBLIC_PUSHER
  })

  const beamsTokenProvider = new PusherPushNotifications.TokenProvider({
    url: `/api/push/register`,
    queryParams: {
      userId
    },
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : undefined)
    }
  })

  beamsClient
    .start()
    .then(() => beamsClient.setUserId(userId, beamsTokenProvider))
    .then(response => console.log(response))
}
heymartinadams commented 2 years ago

This is still the case even if I do a bare-bones function:

<button onClick={() => {
  const beamsClient = new PusherPushNotifications.Client({
    instanceId: process.env.NEXT_PUBLIC_PUSHER
  })

  beamsClient.start().then(response => console.log(response))
}}>
  Enable Notifications
</button>
benw-pusher commented 2 years ago

I've been able to replicate this when using Safari and the code snippet provided. Interestingly the code does work on Firefox, which also has the same restrictions on when Push Notification permission can be granted (in response to a user gesture) so this looks to be specific to Safari. I'll keep digging.

benw-pusher commented 2 years ago

I have been able to resolve this - although I am not certain precisely why this works. Removing the instantiation of the beamsClient from the onClick handler prevents the error from showing. For example, the below works:

let beamsClient;

useEffect(() => {
    beamsClient = new PusherPushNotifications.Client({
     instanceId: process.env.NEXT_PUBLIC_PUSHER
   })
   if("serviceWorker" in navigator) {
     window.addEventListener("load", function () {
      navigator.serviceWorker.register("/service-worker.js").then(
         function (registration) {
           console.log("Service Worker registration successful with scope: ", registration.scope);
         },
         function (err) {
           console.log("Service Worker registration failed: ", err);
         }
       );
     });
   }

   }, []);

   <button onClick={() => {     
    beamsClient.start()
      .then(response => console.log(response))
      .then(() => beamsClient.addDeviceInterest("debug-hello"))
  }}>Enable Notifications
    </button>

Are you able to attempt a similar pattern?

heymartinadams commented 2 years ago

@benw-pusher adding the service worker script to the useEffect, as well as instantiating beamsClient did the trick! I also had to make sure that none of the initialization (beamsClient.start()) was appearing in a useEffect as well. Appears that Safari is quite finicky like that. Including a full reproduction below for authenticated user push notifications in case someone else is running into the same issue.

Thanks again! šŸŽ‰

let beamsClient

const DesignLayoutsTodosNotifications = () => {
  // This `useEffect` apparently must remain in this file, otherwise `beamsClient` is undefined, even if passed into hook file and then exported back
  useEffect(() => {
    beamsClient = new PusherPushNotifications.Client({
      instanceId: process.env.NEXT_PUBLIC_PUSHER
    })

    if ('serviceWorker' in navigator)
      window.addEventListener('load', () =>
        navigator.serviceWorker.register('/service-worker.js').then(
          registration =>
            console.log('Service Worker registration successful with scope: ', registration.scope),
          err => console.log('Service Worker registration failed: ', err)
        )
      )
  }, [])

  return (
    <div>
      <h3>Reminders</h3>

      <button
        onClick={() => {
          // Must remain here and not placed inside a hook so that Safari recognizes this as a user gesture
          const beamsTokenProvider = new PusherPushNotifications.TokenProvider({
            url: `/api/push/register`,
            queryParams: {
              userId: userId // `userId` provided from elsewhere
            },
            headers: {
              Accept: 'application/json',
              'Content-Type': 'application/json',
              ...(token ? { Authorization: `Bearer ${token}` } : {}) // `token` provided from elsewhere
            }
          })

          beamsClient
            .start()
            .then(() => beamsClient.setUserId(userId, beamsTokenProvider))
            .then(() => console.log('Push enabled šŸŽ‰'))
        }}
      >
        Enable Notifications
      </button>
    </div>
  )
}

export default DesignLayoutsTodosNotifications