Amphtml: Intent to Implement: amp-web-push Web push notifications

Created on 15 Apr 2017  Â·  24Comments  Â·  Source: ampproject/amphtml

At OneSignal, we'd like to allow AMP page visitors to subscribe to web push notifications. Here's a demo of web push notifications (regular page, not an AMP page). This AMP extension integrates with our notifications platform service.

Overview

  1. Is the visitor subscribed? Create a hidden iFrame to the site's canonical origin to query the visitor's subscription state

    A user may already be subscribed to push. If subscribed, we don't show subscription widgets. The user's subscription state is stored in the canonical origin's site storage. Because AMP pages can be served on a non-site origin like Google's AMP cache, we create an iFrame to the canonical origin to access this site storage, and use postMessage() to get the user's subscription state.

    The iFrame is hidden and to a lightweight page just for querying subscription state.

  2. Show a subscription widget for unsubscribed visitors

    Developers write an AMP HTML button or link that says "Subscribe to push notifications".

    This subscription widget can be anywhere in the AMP page, guarded by custom visibility attributes which our extension will parse and style the element visibly or invisibly depending on the user's subscription state.

  3. A visitor clicking "Subscribe" is subscribed through a popup window to the canonical origin that loads our JavaScript SDK

    • We can't subscribe for push in an HTTPS iFrame, so we have to do it in on the canonical origin
    • The actual subscription has various conditions and logic (handling different browsers, posting back to our server, registering a worker), so we'd like to load our JavaScript SDK at this point that normally handles subscribing users on non-AMP pages. The SDK only loads for users clicking "Subscribe".
  4. Widget disappears, and subscriber resumes browsing

    The next time the subscriber visits this page, the subscription widget will be hidden.

Example

Code so far is available in our forked version. [Not completed! In the beginning stages.]

Developers set the iFrame URL through a <script type='application/json'>. This must match the AMP page's canonical origin to prevent linking to an unrelated site.

  <!-- OneSignal AMP Extension Configuration
    ampHelperUrl: Provide an absolute URL to the subscription helper page. (e.g. 'https://my-site.com/onesignal-amp-check'). This must match your AMP Page's canonical origin.
  -->
  <script id="amp-onesignal-config" type="application/json">
    {
        "ampHelperUrl": "https://my-site.com/onesignal-amp-check"
    }
  </script>

We load https://my-site.com/onesignal-amp-check in a hidden and sandboxed allow-same-origin allow-scripts iFrame and postmessage to check their subscription status.

Their AMP HTML, with our extension-specific attributes to hide or show their custom subscription widgets based on their subscription status:

```
Some article text.




```
If the visitor clicks subscribe, we open a popup to https://my-site.com/onesignal-amp-check?subscribe which has code in place to dynamically load our JavaScript SDK (loaded only once the user clicks subscribe, in this popup separate from the AMP document) and subscribe the user to notifications.

Whether the user denies or grants permissions, the popup will close, leaving the user on the original AMP page.

INTENT TO IMPLEMENT DiscussioQuestion access-subscriptions

All 24 comments

What would it take to generalise this to work for other services that provide push notifications on the web?

Why does this need to be a custom component? It sounds like all use cases are already covered by amp-access.

Hey @sebastianbenz,

I think amp-access checks for a JSON { success: true } response from an authorization URL a developer defines, but we need to access client-sided storage data, so the authorization URL the developer defines needs to be a page that runs some JS to check the storage and then postMessage() the result back. A server-only response wouldn't allow JavaScript to access the site's storage state.

Hey @adewale,

We'll brainstorm how to generalize this to fit other push vendors and web notifications more generally.

@jasonpang you can check notification state via cookies in the auth endpoint. The auth request can return arbitrary JSON, e.g. { "notification": "permission_denied" }. You can set the cookie in the subscribe popup, which you can trigger via amp-access login action.

Hey @sebastianbenz,

As you mentioned, amp-access checks subscription state by making a request to the origin server, and we could have the server detect a "user is subscribed" cookie passed along with the request.

But tracking subscription state using cookies isn't as accurate and can't reflect the user's actual subscription state.

  • A visitor successfully subscribed to web push notifications who clears their cookies will be prompted to resubscribe even though only clearing cookies doesn't affect the user's actual push subscription. A more accurate check would be the origin's PushManager.getSubscription() return value (null if unsubscribed, otherwise it will have an endpoint). More accurate tracking can give site devs an opportunity to provide a better user experience.

Also, amp-access's request —> JSON response strategy has some disadvantages for a push subscription-only model.

  • For greater developer adoption of AMP, it's more practical to provide a ready-made file (to embed invisibly and postMessage() messages across) than to ask developers to create an API endpoint.

Hey @adewale,

We'd be happy to generalize this component as a web push notifications AMP extension (<amp-web-push>) for all vendors. Based on the existing proposal, this can be easily done with vendors customizing the iFrame and popup HTML page to perform vendor-specific actions. For example:

  • Checking a visitor's subscription

    The iFrame replies true or false.

    The iFrame page we provide can be tweaked per-vendor to implement isSubscribed() differently. For example, for our service, we'd additionally check an IndexedDb flag that describes whether a user prefers to temporarily mute notifications. Other vendors may check other preferences.

  • Determining whether to show the subscription widget

    The iFrame replies true or false.

    Vendors have the flexibility to respond to a variety of user states to provide the best user experience. For example, we can detect:

    • New: Never subscribed, never visited (suggestion: don't prompt the user to subscribe yet)
    • Visiting: Never subscribed, previously visited (suggestion: maybe prompt this user to subscribe)
    • Previously subscribed: Notification permission granted, but no subscription data (suggestion: prompt cautiously)
    • Unsubscribed: Notification permission blocked, or notification permission default ask but existing stored subscription data (varies by vendor, suggestion: reprompt very rarely)
    • Subscribed
  • Subscribing the visitor

    A popup window opens to a new page to subscribe the user.

    Vendors can provide a tweaked popup page to register their uniquely named service worker.

    For the best user experience, minimal work is done in the popup and the popup is closed quickly. Parts of the subscription, like posting to the push vendor to store subscription details, can be done after in the iFrame.

If this proposal looks good, should we continue working on getting out a small proof of concept?

Hey @adewale @sebastianbenz,

We've been hard at work on a proof of concept to demo an early version of our AMP extension and how it would work. We're happy to show a demo of an alpha version of our plugin!

amp-web-push Demo

Demo Link

AMP Push Notifications Extension Demo

Demo Walkthrough

  1. You should see a blue _Subscribe to Notifications_ button.

    image

    • We open a hidden, lightweight, sandboxed iFrame to the canonical origin

      image

      The frames accepts queries from the host page: e.g., should the visitor see the subscription widget?

      image

      Each push vendor provides their own implementation of isSubscribedToPushNotifications(). Sites modify the above code to control the visibility of subscription widgets.

    • This button _subscription widget_ is defined by the AMP site

      image

    • Sites can add multiple named subscription widgets

      Here's a _Thanks for subscribing!_ widget, shown only after subscribing

      image

      This way, users can customize the amount of widgets to display, the location of each widget, and the visibility of each widget.

  2. Click _Subscribe to Notifications_. A popup window will open.

    image

    • Both the popup and frame URLs are preset in an AMP configuration section, similar to amp-access

      image

    • The popup, like the iframe, communicates via postMessage() to indicate when the subscription is complete or canceled

    • Each push vendor provides their own implementation of the subscription popup

    • You can click X to cancel the permission request. You can also block it (learn how to grant permissions again here). Eventually, Allow the notification permission.

  3. You're now subscribed!

    image

    • Once the popup closes, widget visibilities update. The user may have blocked permissions or subscribed successfully; both actions hide the subscription widgets.

Demo Resources


We look forward to any feedback you have regarding our implementation!

Here is the amp-web-push extension source code.

Aside from adding tests, adding types via comments, cleaning up the code a bit, this code is how we envision the first release of the plugin to be.

Note: Although the first link you're opening looks like the same domain as the popup that opens, the first page is supposed to represent an AMP page on something like Google's cache. It should work even if the domain is completely different and it's a full-on AMP page.

Sorry for the late response 😞. Somehow this slipped through my filter and then I've been on vacation for a while.

Adding @cramforce @dvoytenko @ericlindley-g for feedback.

Two things that come to my mind:

  • to avoid layout jumps on load you'd need to ensure that the different notification status widget have the same dimensions.
  • this approach makes it possible to run arbitrary code in a hidden iframe

Would it be possible to embed your notification widget via amp-iframe?

A few thoughts:

  • using localStorage will not work in Safari once they support push notifications.
  • cookies would work; lets use cookies and avoid loading an iframe just to query subscription state.
  • Popups are much more awkward on mobile than they are on desktop. What is displayed in the popup?

/cc @aghassemi for visibility as well

@jasonpang — do you have a new link to the demo? Would like to check out the UI if it's easy to get running again.

@sebastianbenz @cramforce @ericlindley-g thanks for your replies!

We've made another demo available at this link (with a GIF recording just in case):


@sebastianbenz:

  • What are best practices on avoiding layout reflows using static widget dimensions?

    To prevent reflow, should we display an empty placeholder space while the widget calculates whether it should display itself? If no widget is eventually displayed (e.g. user already subscribed), the empty space would persist on the page (removing the empty space would cause layout jumps) and look irregular.

    But without an empty placeholder space, there would be layout jumps as our widget inserts itself on the page.

  • Running arbitrary code in a hidden iframe seems unavoidable

    It doesn't seem possible to prevent this, and our approach shouldn't be any less secure than using amp-iframe.

    amp-iframe also makes it possible to run arbitrary code, and as long as an iFrame is used, users can run custom scripts.

    Some restrictions we can add: our extension can refuse to load a third-party URL (to be added) by requiring the origin to match the site's canonical origin. Despite our best efforts to check the URL, developers can still point to a custom script hosted on their origin, so this seems like the best we can do. Our hidden iFrame lives on the site's canonical origin, so scripts won't interfere with the actual AMP page.

  • Embedding our notification widget via amp-iframe won't work

    amp-iframe's origin policy doesn't allow same-origin URLs, but we need to load the same-origin URL to access the Notification API permission state.


@cramforce:

  • We'll disable Safari AMP push support for now

    We'll disable Safari support for now since Safari push isn't supported on mobile, but we'll revisit the problem of 3rd party cookies in a iFrame once Safari supports push.

  • Using cookies to track subscription isn't as accurate as using an iFrame

    Using cookies would provide basic subscription tracking, but doesn't take advantage of the much more accurate subscription tracking offered by the Notifications, Push, and Service Worker APIs.

    For example, if using cookies to track subscription state, a visitor subscribed to web push notifications who clears their cookies will be prompted to resubscribe (because the cookie would
    be our only check for an AMP push subscription), even though clearing cookies doesn't impact the user's actual push subscription.

    Instead, we can more accurately and directly check the components of a push subscription. For example, in our implementation, we check whether the service worker registration is active, notification permissions are granted, and the push token is not null. This check directly reflects the actual subscription state.

  • Popup content should minimally display the permission prompt

    Sorry the demo wasn't online at the time (should be up now!). The popup exists primarily to show the notification request, which can't be requested from within an iFrame. Developers and push vendors can customize the subscription popup (which lives on the canonical origin) with extra text.

This sounds good to me. I'd definitely prefer if this component was vendor independent.

Let's move forward, but let's also plan for some time to iterate on the final design during implementation.

Pinging @jasonpang @sebastianbenz @cramforce @ericlindley-g

As another vendor of push notification services, Mobify have been looking at this and we'd like to contribute.

  1. Using storage to detect the subscription status seems open to race conditions: the storage result could be "subscribed" but the user might have blocked or reset notification permission since the last visit to the site (we see that users will remove permission via the Settings icon on the notification, meaning that subscriptions are removed without any site code being run). We agree that the most reliable way to test would be to check Notification.permission and call PushManager.getSubscription() on the canonical site, and an iframe seems like the only way to do this.
  2. The proposal has just one boolean value for isSubscribedToPushNotifications. It's likely that other implementations (such as ours) will need more data (for example, we record different notification sources that the user may be subscribed to). Maybe this 'check' call should return an object that can contain arbitrary metadata? Also, the subscription state might be better represented with isSubscribed and canSubscribe booleans.

Additional: it's probably worth noting that the subscribe popup would also have to register and install the service worker in order to set up a subscription.

@benlast

We found a somewhat vendor independent approach (to end users), with the following benefits:

  • Developers don't need to write custom JavaScript code

    They still need to add files (that we provide) to their site (no way around this due to same-origin push policy), but we provide these files without requiring any extra modifications. Better yet, custom modifications (displaying supporting text or graphs with the browser's permission request to encourage subscription) is still possible, just not required.

    In our original impl., users would have had to consult their push vendor's docs for a special set of files.

  • The end-user visible subscription process is significantly faster (popup closes immediately after granting permission vs. waiting seconds)

    This is possible because we're outsourcing the actual subscription to the service worker to run in the background, instead of subscribing inside the popup and waiting for the operation to complete.

The disadvantage is that this shifts the complexity to push vendors, which would have to allow subscribing through the service worker.

Our new approach outsources vendor-specific code to the vendor's service worker (which must always be installed anyways as part of web push), which already runs other vendor-specific code. Push vendors must subscribe for push in the service worker and broadcast a signal to controlled window clients when complete.

This way, the process looks like:

#### New Way

  1. User clicks widget to subscribe.
  2. Service worker is registered through the iFrame (headstart so it activates more quickly).
  3. Popup window opens to script on canonical origin. Service worker should be activated by this point.
  4. Call Notification.requestPermission() (merged with Push API permission, does not require SW to be active). The browser's notification permission request appears.
  5. User grants permission.
  6. Popup window closes immediately (subscription is _not_ complete yet). AMP extension sends message to iFrame to complete the subscription.
  7. The iFrame doesn't do anything, it just forwards the message to the service worker to complete the subscription.
  8. All vendor-specific subscription occurs in the service worker. Permissions are already granted, so the service worker can subscribe here (e.g. self.registration.pushManager.subscribe())
  9. Service worker sends a message back to the iFrame that subscription is complete (self.clients.matchAll() ... client.postMessage())
  10. iFrame tells AMP extension subscription is complete.

#### Old Way

  1. User clicks widget to subscribe.
  2. Popup window opens to script on canonical origin.
  3. Subscription partially completes here, non-visible portions complete in the iFrame. Popup window closes.
  4. iFrame completes rest of non-visible subscription (e.g. calling API to push vendor).

This old way requires different script files on the canonical origin for each push vendor. It also makes the popup open for longer (to register the worker and make the subscription call).

@benlast Forgot to reply to extra values/state for isSubscribedToPushNotifications() (your comment #2).

Instead of the iFrame telling the AMP extension whether the user is subscribed or can be subscribed, the iFrame actually only tells the widget whether to _show_ the widget. This way, any custom logic for different use cases can be combined to make a final decision on the widget visibility.

We were thinking: if a widget is visible, it can be clicked and the user will be subscribed. If the user shouldn't be subscribed for any reason (e.g. already subscribed, blocked, dismissed multiple times), the widget shouldn't be visible.


We're thinking of holding a design review on 7/5. Would you like to join the review to discuss all this more?

I'll paste a design document on the 7/5 Design Review soon.

@jasonpang yes, I'd like to join the review.

@jasonpang a more detailed version of what I was thinking as discussed in the meeting today.

// amp-webpush-subscribe-widget is restricted to fixed-size layouts only 
// (e.g. no layout=container) so no page jumps are possible.
<amp-webpush-subscribe-widget>
  <div notsubscribed>
     Click here to subscribe.
  </div>
  <div subscribed>
     You are subscribed to notifications, if you like to unsubscribe, use browser settings 
     (or if we support unsubscribe ever, this can be "click here to unsubscribe")
  </div>
</amp-webpush-subscribe-widget>

<div subscribed> is optional, if not provided, <div notsubscribed> will simply become hidden and widget becomes empty. Ideally we can detect whether we can also collapse the whole <amp-webpush-subscribe-widget> when it is empty (e.g. if not inside viewport or in a position fixed parent)

I spoke to @beverloo regarding Chrome potentially tightening up various requirements around requesting permission (see https://github.com/WICG/interventions/issues/49).

To summarize:

  • The complexities around one site hosting the content, and a separate site hosting the push notification "plumbing" (service worker, requesting notification permission, etc.) as required by the <amp-webpush> component are not unique to AMP, and Chrome are very keen to make this split-domain approach work in a reasonable way.
  • Notwithstanding the above, Chrome is likely to begin experimenting with preventing sites with a low site engagement score from seeking permission to display notifications in M62 (and potentially M61), which will mean that affected users will need to perform three actions to get push notifications: click "subscribe" on the AMP page (which opens a non-AMP page with a low engagement score), something to generate "engagement" on the non-AMP page (e.g. click "yes, I'm sure I want notifications"), and finally consenting to notifications via the Chrome UI.

So, however <amp-webpush> is implemented, and whatever its API, I think the component will need to be prepared to live with these platform-enforced constraints, and a less-than-ideal UX.

@ithinkihaveacat

Regarding your comment on "experimenting" with preventing sites with a low engagement score from seeking notification permissions..

What exactly does "experimenting" mean in this context? Is this something that'll look to be actively enforced and turned on in M62/M61?

@henryh15 I don't have the details but "experimenting" is likely to mean that a small proportion of users (in the stable channel) will get the new behavior.

Hey All @jasonpang and the rest of the involved users, the community is looking forward to having this. Let's make this happen and keep us updated.

Regards,
Ahmed

This issue seems to be in Pending Triage for awhile. @rudygalfi Please triage this to an appropriate milestone.

Was this page helpful?
0 / 5 - 0 ratings