Using the web SDK with AMP

Guide to implement the Xtremepush SDK on AMP pages

AMP, or Accelerated Mobile Pages, is a Google-backed project that uses a stripped down and lightweight version of HTML to allow for faster content for mobile users.

The following guide will show you how you can subscribe to push notifications using the Xtremepush web SDK on an AMP page.

You must be able to host the necessary files on your HTTPS-enabled website.

Add the web-push plugin

In the page HEAD include the AMP web-push script:

<script async custom-element="amp-web-push" src="https://cdn.ampproject.org/v0/amp-web-push-0.1.js"></script>

Add the following to the page BODY, replacing YOUR_DOMAIN with your actual website domain to reference the files you will add next:

<amp-web-push id="amp-web-push"
             layout="nodisplay"
             helper-iframe-url="https://YOUR_DOMAIN.com/helper-frame.html"
             permission-dialog-url="https://YOUR_DOMAIN.com/permission-dialog.html"
             service-worker-url="https://YOUR_DOMAIN.com/service-worker.js"
             service-worker-scope="/">
</amp-web-push>

Add required files

Now we need to add the various files referenced above.

helper-frame.html

This is the page we will be adding the Xtremepush SDK integration code. So you will need to grab that from the SDK integration section on the Xtremepush platform.

Your code will look like the following:

<!doctype html>
<html>
<!-- AMP Web Push Helper IFrame -->

<head>
 <meta charset="utf-8">

 <script type="text/javascript">
   (function(p,u,s,h,e,r,l,i,b) {p['XtremePushObject']=s;p[s]=function(){
     (p[s].q=p[s].q||[]).push(arguments)};i=u.createElement('script');i.async=1;
     i.src=h;b=u.getElementsByTagName('script')[0];b.parentNode.insertBefore(i,b);
   })(window,document,'xtremepush','path/to/sdk.js');
 </script>

</head>
<body>
</body>
</html>

permission-dialog.html

Add the following as an HTML file to your root directory:

<!doctype html>
<html>
<!-- AMP Web Push Permission Dialog -->

<head>
   <meta charset="utf-8">

   <!-- Do not edit styles in this section -->
   <style builtin>
   /* Do not edit styles in this section. Add custom styles in the next
      style section */
   html, body {
     height: 100%;
     margin: 0;
     padding: 0;
   }

   /* Default loading spinner */
   .spinner,
   .spinner:after {
     border-radius: 50%;
     width: 10em;
     height: 10em;
   }
   .spinner {
     margin: 60px auto;
     font-size: 10px;
     position: relative;
     text-indent: -9999em;
     border-top: 1.1em solid rgba(140, 140, 140, 0.2);
     border-right: 1.1em solid rgba(140, 140, 140, 0.2);
     border-bottom: 1.1em solid rgba(140, 140, 140, 0.2);
     border-left: 1.1em solid rgb(140, 140, 140);
     -webkit-transform: translateZ(0);
     -ms-transform: translateZ(0);
     transform: translateZ(0);
     -webkit-animation: spinner 1.1s infinite linear;
     animation: spinner 1.1s infinite linear;
   }
   @-webkit-keyframes spinner {
     0% {
       -webkit-transform: rotate(0deg);
       transform: rotate(0deg);
     }
     100% {
       -webkit-transform: rotate(360deg);
       transform: rotate(360deg);
     }
   }
   @keyframes spinner {
     0% {
       -webkit-transform: rotate(0deg);
       transform: rotate(0deg);
     }
     100% {
       -webkit-transform: rotate(360deg);
       transform: rotate(360deg);
     }
   }

   /* Horizontally and vertically center items */
   body {
     display: flex;
     justify-content: center;
     align-items: center;
     padding: 15px;
   }

   /* Page message text styles */
   .message {
     font-size: 22px;
     font-family: sans-serif;
     text-align: center;
   }

   /* Close icon styles */
   #close {
     position: fixed;
     right: 32px;
     top: 32px;
     width: 32px;
     height: 32px;
     opacity: 0.5;
     cursor: pointer;
   }
   #close:before, #close:after {
     position: fixed;
     right: 48px;
     content: ' ';
     height: 33px;
     width: 3px;
     background-color: #333;
   }
   #close:before {
     transform: rotate(45deg);
   }
   #close:after {
     transform: rotate(-45deg);
   }

   /* Used to hide the preload and postload sections */
   .invisible {
     display: none;
   }
 </style>

 <!-- Optional: Add custom styles here -->
 <style custom>
   /* Add custom styles here */
 </style>
</head>
<body>
<div id="preload">
   <!-- Anything in this section will be shown before the main script
         runs, and will be hidden after -->
   <div class="spinner"></div>
</div>
<div id="postload" class="invisible">
   <span id="close"></span>
   <div permission="default">
       <p class="message">
           <!--
           Customize the subscribing message here
               (e.g. "Click Allow to receive notifications")
           -->
           Click Allow to receive notifications.
       </p>
   </div>
   <div permission="denied">
       <!--
         Customize the unblocking message here
             (e.g. "Please unblock notifications in your browser settings
                     to receive notifications from this website.")
         -->
       <p class="message">
           Please unblock notifications in your browser settings to receive notifications from this website.
       </p>
   </div>
</div>
</body>
</html>
<script>
(function(){var f,h=function(a){return"undefined"!=typeof window&&window===a?a:"undefined"!=typeof global&&null!=global?global:a}(this);function k(a,b){b=void 0===b?"":b;try{return decodeURIComponent(a)}catch(d){return b}};var l=/(?:^[#?]?|&)([^=&]+)(?:=([^&]*))?/g;self.log=self.log||{user:null,dev:null,userForEmbed:null};var m=self.log;/*
https://mths.be/cssescape v1.5.1 by @mathias | MIT license */
var n=/(\0)|^(-)$|([\x01-\x1f\x7f]|^-?[0-9])|([\x80-\uffff0-9a-zA-Z_-]+)|[^]/g;function p(a,b,d,c,e){return e?e:b?"\ufffd":c?a.slice(0,-1)+"\\"+a.slice(-1).charCodeAt(0).toString(16)+" ":"\\"+a};function t(a){this.K=a;this.A=this.F=0;this.m=Object.create(null)}t.prototype.has=function(a){return!!this.m[a]};t.prototype.get=function(a){var b=this.m[a];if(b)return b.access=++this.A,b.payload};
t.prototype.put=function(a,b){this.has(a)||this.F++;this.m[a]={payload:b,access:this.A};if(!(this.F<=this.K)){if(m.dev)a=m.dev;else throw Error("failed to call initLogConstructor");a.warn("lru-cache","Trimming LRU cache");a=this.m;var d=this.A+1,c,e;for(e in a){var g=a[e].access;g<d&&(d=g,c=e)}void 0!==c&&(delete a[c],this.F--)}};var u,v;
function w(a){var b;u||(u=self.document.createElement("a"),v=self.UrlCache||(self.UrlCache=new t(100)));var d=b?null:v,c=u;if(d&&d.has(a))a=d.get(a);else{c.href=a;c.protocol||(c.href=c.href);var e={href:c.href,protocol:c.protocol,host:c.host,hostname:c.hostname,port:"0"==c.port?"":c.port,pathname:c.pathname,search:c.search,hash:c.hash,origin:null};"/"!==e.pathname[0]&&(e.pathname="/"+e.pathname);if("http:"==e.protocol&&80==e.port||"https:"==e.protocol&&443==e.port)e.port="",e.host=e.hostname;e.origin=
c.origin&&"null"!=c.origin?c.origin:"data:"!=e.protocol&&e.host?e.protocol+"//"+e.host:e.href;d&&d.put(a,e);a=e}return a};function x(a){a||(a={debug:!1,windowContext:window});this.l={};this.j={};this.H=a.debug;this.o=this.L=this.M=!1;this.w=this.C=this.D=this.h=this.B=null;this.c=a.windowContext||window}f=x.prototype;
f.listen=function(a){var b=this;return(new Promise(function(d,c){b.o?c(Error("Already connected.")):b.M?c(Error("Already listening for connections.")):Array.isArray(a)?(b.D=b.O.bind(b,a,d,c),b.c.addEventListener("message",b.D)):c(Error("allowedOrigins should be a string array of allowed origins to accept messages from. Got:",a))})).then(function(){b.send(x.Topics.CONNECT_HANDSHAKE,null);b.o=!0})};
f.O=function(a,b,d,c){var e=c.data,g=c,q=g.ports;a:{for(var g=w(g.origin).origin,r=0;r<a.length;r++)if(w(a[r]).origin===g){a=!0;break a}a=!1}a&&e&&e.topic===x.Topics.CONNECT_HANDSHAKE&&(this.c.removeEventListener("message",this.D),this.h=q[0],this.w=this.I.bind(this),this.h.addEventListener("message",this.w,!1),this.h.start(),b())};
f.connect=function(a,b){var d=this;return new Promise(function(c,e){a||e(Error("Provide a valid Window context to connect to."));b||e(Error("Provide an expected origin for the remote Window or provide the wildcard *."));d.o?e(Error("Already connected.")):d.L?e(Error("Already connecting.")):(d.B=new MessageChannel,d.h=d.B.port1,d.C=d.N.bind(d,d.h,b,c),d.h.addEventListener("message",d.C),d.h.start(),a.postMessage({topic:x.Topics.CONNECT_HANDSHAKE},"*"===b?"*":w(b).origin,[d.B.port2]))})};
f.N=function(a,b,d){this.o=!0;a.removeEventListener("message",this.C);this.w=this.I.bind(this);a.addEventListener("message",this.w,!1);d()};f.I=function(a){a=a.data;if(this.l[a.id]&&a.isReply){var b=this.l[a.id];delete this.l[a.id];var d=b.promiseResolver;b.message=a.data;d([a.data,this.J.bind(this,a.id,b.topic)])}else{var c=this.j[a.topic];if(c)for(var e=0;e<c.length;e++)(0,c[e])(a.data,this.J.bind(this,a.id,a.topic))}};f.on=function(a,b){this.j[a]?this.j[a].push(b):this.j[a]=[b]};
f.off=function(a,b){if(b){var d=this.j[a].indexOf(b);-1!==d&&this.j[a].splice(d,1)}else this.j[a]&&delete this.j[a]};f.J=function(a,b,d){var c=this,e={id:a,topic:b,data:d,isReply:!0};this.h.postMessage(e);return new Promise(function(a){c.l[e.id]={message:d,topic:b,promiseResolver:a}})};f.send=function(a,b){var d=this,c={id:crypto.getRandomValues(new Uint8Array(10)).join(""),topic:a,data:b};this.h.postMessage(c);return new Promise(function(e){d.l[c.id]={message:b,topic:a,promiseResolver:e}})};
h.Object.defineProperties(x,{Topics:{configurable:!0,enumerable:!0,get:function(){return{CONNECT_HANDSHAKE:"topic-connect-handshake",NOTIFICATION_PERMISSION_STATE:"topic-notification-permission-state",SERVICE_WORKER_STATE:"topic-service-worker-state",SERVICE_WORKER_REGISTRATION:"topic-service-worker-registration",SERVICE_WORKER_QUERY:"topic-service-worker-query",STORAGE_GET:"topic-storage-get"}}}});function y(a){this.H=a&&a.debug;this.c=a.windowContext||window;this.G=new x({debug:this.H,windowContext:this.c})}f=y.prototype;f.isCurrentDialogPopup=function(){return!!this.c.opener&&this.c.opener!==this.c};f.requestNotificationPermission=function(){var a=this;return new Promise(function(b,d){try{a.c.Notification.requestPermission(function(a){return b(a)})}catch(c){d(c)}})};
f.run=function(){z(this);A(this);for(var a=this.c.document.querySelectorAll("[permission]"),b=0;b<a.length;b++)B(a[b],!1);(a=this.c.document.querySelector("[permission="+String(this.c.Notification.permission).replace(n,p)+"]"))&&B(a,!0);a=this.c.document.querySelector("#preload");b=this.c.document.querySelector("#postload");a&&b&&(B(a,!1),B(b,!0));"denied"!==this.c.Notification.permission?C(this):D(this)};
function z(a){var b=a.c.document.querySelector("#close");b&&b.addEventListener("click",function(){a.closeDialog()})}f.closeDialog=function(){if(this.isCurrentDialogPopup())this.c.close();else{var a=this.c.fakeLocation||this.c.location;var b=a.search,d=Object.create(null);if(b)for(var c;c=l.exec(b);){var e=k(c[1],c[1]);c=c[2]?k(c[2],c[2]):"";d[e]=c}var g=d;if(!g["return"])throw Error("Missing required parameter.");var q=k(g["return"],void 0);this.redirectToUrl(q)}};
function D(a){navigator.permissions.query({name:"notifications"}).then(function(b){b.onchange=function(){A(a);switch(a.c.Notification.permission){case "default":case "granted":C(a)}}})}function A(a){a.c.localStorage.setItem("amp-web-push-notification-permission",a.c.Notification.permission)}function B(a,b){if(a){var d="invisible";b?a.classList.remove(d):a.classList.add(d)}}
function C(a){a.requestNotificationPermission().then(function(b){A(a);if(a.isCurrentDialogPopup())return a.G.connect(opener,"*"),a.G.send(x.Topics.NOTIFICATION_PERMISSION_STATE,b).then(function(b){(b=b[0])&&b.closeFrame&&a.closeDialog()});a.closeDialog()})}f.redirectToUrl=function(a){var b=w(a);!b||"http:"!==b.protocol&&"https:"!==b.protocol||(this.c.location.href=a)};window._ampWebPushPermissionDialog=new y({debug:!1});window._ampWebPushPermissionDialog.run();})();
//# sourceMappingURL=amp-web-push-permission-dialog.js.map
</script>

service-worker.js

Add the service worker code found on the SDK integration page by navigating to Settings > Apps & sites > click on the matching website > Push Settings. Find an example of the code below.

📘

The script for the service worker file will only be shown in Push Settings if you have selected the Use Self-hosted Domain option, but the service worker is required for AMP integration.

importScripts(decodeURIComponent(
  location.search.substring(location.search.indexOf('ref=')+4)
));

Prompting

If auto-prompting is disabled, you can prompt the user from your AMP page by calling tap:amp-web-push.subscribe. For example, a button with that action:

<button on="tap:amp-web-push.subscribe">Prompt User</button>