PWA Installation + Push notifications in Bubble - What works for me

Dear Bubblers’ I’ve recently seen many topics of people requesting help for PWA and Push Notification support in Bubble and I’d like to share with the community how I have managed to make things work.

I’m certain that this implementation can be made more efficient, and if you know how to do so, please pitch in so we can all make a definitive go-to manual for this.

I do have to say that this has been built by trial and error, and using very valuable input from other bubblers in this forum which at the moment I don’t recall, but if I where to find those posts, I’ll make sure to tag them along.

I have been able to get this approach to work on 4 of my 5 production apps, the 5th one throws constant errors and I had to end up removing PWA support (bubble wouldn’t get involved since it’s all custom code).

First of all, you must be familiar with uploading/hosting files to the root directory, for this, you must go to “Settings” - “SEO / metatags”. In the bottom part you will find the field “Hosting files in the root directory”.

So, we must get things ready, and understand that you’ll need your app icon files plus manifest.json file, sw.js and main.js files

First, we must generate our icons, I use https://appiconmaker.co/ service which is perfect for this, and download the following PNGs:

  • icon-57.png
  • icon-60.png
  • icon-72.png
  • icon-76.png
  • icon-114.png
  • icon-120.png
  • icon-144.png
  • icon-152.png
  • icon-192.png
  • icon-512.png

Yes, these are WAY more than we really need, but I prefer to have more than actually required just in case of different use cases/navigators/OS’s.

When downloading all of these icons, upload them to the root directory, you will need the URL for each to use in the manifest file next.

manifest.json

{
    "name": "Your App Name",
    "short_name": "Short App Name",
    "icons": [
      {
        "src": "https://s3.amazonaws.com/appforest_uf/xxxxxxxxxxxxxx/icon-57.png",
        "sizes": "57x57",
        "type": "image/png"
      },
      {
        "src": "https://s3.amazonaws.com/appforest_uf/xxxxxxxxxxxxxx/icon-60.png",
        "sizes": "60x60",
        "type": "image/png"
      },
      {
        "src": "https://s3.amazonaws.com/appforest_uf/xxxxxxxxxxxxxx/icon-72.png",
        "sizes": "72x72",
        "type": "image/png"
      },
      {
        "src": "https://s3.amazonaws.com/appforest_uf/xxxxxxxxxxxxxx/icon-76.png",
        "sizes": "76x76",
        "type": "image/png"
      },
      {
        "src": "https://s3.amazonaws.com/appforest_uf/xxxxxxxxxxxxxx/icon-114.png",
        "sizes": "114x114",
        "type": "image/png"
      },  
      {
        "src": "https://s3.amazonaws.com/appforest_uf/xxxxxxxxxxxxxx/icon-120.png",
        "sizes": "120x120",
        "type": "image/png"
      },
      {
        "src": "https://s3.amazonaws.com/appforest_uf/xxxxxxxxxxxxxx/icon-144.png",
        "sizes": "144x144",
        "type": "image/png"
      },
      {
        "src": "https://s3.amazonaws.com/appforest_uf/xxxxxxxxxxxxxx/icon-152.png",
        "sizes": "152x152",
        "type": "image/png"
      },
      {
        "src": "https://s3.amazonaws.com/appforest_uf/xxxxxxxxxxxxxx/icon-192.png",
        "sizes": "192x192",
        "type": "image/png"
      },
      {
        "src": "https://s3.amazonaws.com/appforest_uf/xxxxxxxxxxxxxx/icon-512.png",
        "sizes": "512x512",
        "type": "image/png"
      }
    ],
    "start_url": "https://yourAppDomain.com",
    "display": "standalone",
    "background_color": "#desiredBackgroundColor",
    "theme_color": "#yourThemeColor"
}

Some would say these are too many icons, but I prefer to have more than strictly required to account for most use cases. Copy this in to a text editor (I use Sublime Text) and save it as manifest.json. Do update the required data with your own. The src: fields are all from the icons you previously uploaded to the root directory.

Now, for the service workers:

sw.js

'use strict';

self.addEventListener('push', function(event) {
   console.log('[Service Worker] Push Received.');
   console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);
const title = 'Push Codelab';
   const options = {
 body: event.data.text(),
 icon: 'icon-192.png',
 badge: 'icon-192.png'
   };
event.waitUntil(self.registration.showNotification(title, options));
 });

self.addEventListener('notificationclick', function(event) {
 console.log('[Service Worker] Notification click Received.');

 event.notification.close();

 event.waitUntil(
 clients.openWindow('https://yourAppDomain.com')
 );
});

// CACHE
const cacheName = 'cache-v1';
const resourcesToPrecache = [
   '/',
   "index.html"
];

self.addEventListener('install', event => {
   console.log("SW install event");
   event.waitUntil(
   caches.open(cacheName)
       .then(cache => {
           return cache.addAll(resourcesToPrecache);
       })
       .catch(err => {
           console.log(err);
       })
   );
});

self.addEventListener('fetch', event => {
   event.respondWith(caches.match(event.request)
   .then(cachedResponse => {
       return cachedResponse || fetch(event.request);
   })
   );
});

Again, copy this code into the text editor and save it as sw.js. This is your service worker declaration. Do replace your app URL, icon: and badge: fields with the icon file name you are going to use for the app (same as used in the manifest file). I use the 192x192 size icon which works well in android/ios devices.

Finally:

main.js

'use strict';

let swRegistration = null;

if ('serviceWorker' in navigator && 'PushManager' in window) {
 console.log('Service Worker and Push is supported');

 navigator.serviceWorker.register('sw.js')
 .then(function(swReg) {
   console.log('Service Worker is registered', swReg);

   swRegistration = swReg;
 })
 .catch(function(error) {
   console.error('Service Worker Error', error);
 });
} else {
 console.warn('Push messaging is not supported');
}

The main.js file is the one responsible for registering the service workers. As you can see these specific codes serve the purpose for both Push notification activation (OneSignal is required you can follow the OneSignal plugin tutorial for this) and App Install.

Upload them all to the root directory.

In the end, your SEO / metatags files should look something like this (I replaced Icon with logo on my file names, but you can use the original file names as described above):

Next, we must instruct the app to fetch the manifest file with the following code in the “scripts/meta tags in header” field in the same “SEO / metatags” tab:

<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" sizes="152x152" href="logo-152.png">
<link rel="apple-touch-icon" sizes="120x120" href="logo-120.png">
<meta name="apple-mobile-web-app-status-bar-style" content="default">

The apple touch icon sizes are intended for iOS devices + setting the safari navigation bar hidden when the app is saved in the home screen.

Now, I have found that to get OneSignal to work properly, you must run a javascript code just before using the plugin action: “OneSignal - show notification prompt”. For this, add an action to the “When page is loaded” workflow to run javascript with the following code:

'use strict';

if ('serviceWorker' in navigator && 'PushManager' in window) {
 console.log('Service Worker and Push is supported');

 navigator.serviceWorker.register('sw.js')
 .then(function(swReg) {
   console.log('Service Worker is registered', swReg);

   swRegistration = swReg;
   initializeUI();
 })
 .catch(function(error) {
   console.error('Service Worker Error', error);
 });
} else {
 console.warn('Push messaging is not supported');
 pushButton.textContent = 'Push Not Supported';
}

function initializeUI() {
 pushButton.addEventListener('click', function() {
   pushButton.disabled = true;
   if (isSubscribed) {
     unsubscribeUser();
   } else {
     subscribeUser();
   }
 });

 // Set the initial subscription value
 swRegistration.pushManager.getSubscription()
 .then(function(subscription) {
   isSubscribed = !(subscription === null);

   if (isSubscribed) {
     console.log('User IS subscribed.');
   } else {
     console.log('User is NOT subscribed.');
   }

   updateBtn();
 });
}

function updateBtn() {
 if (Notification.permission === 'denied') {
   pushButton.textContent = 'Notificaciones Bloqueadas';
   pushButton.disabled = true;
   updateSubscriptionOnServer(null);
   return;
 }

 if (isSubscribed) {
   // pushButton.style.display = 'none';
   pushButton.textContent = 'Desactivar Notificaciones';
 } else {
   pushButton.textContent = 'Activar Notificaciones';
 }

 pushButton.disabled = false;
}

function subscribeUser() {
 const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
 swRegistration.pushManager.subscribe({
   userVisibleOnly: true,
   applicationServerKey: applicationServerKey
 })
 .then(function(subscription) {
   console.log('User is subscribed.');
   updateSubscriptionOnServer(subscription);
   isSubscribed = true;
   updateBtn();
 })
 .catch(function(err) {
   console.log('Failed to subscribe the user: ', err);
   updateBtn();
 });
}

function unsubscribeUser() {
 swRegistration.pushManager.getSubscription()
 .then(function(subscription) {
   if (subscription) {
     return subscription.unsubscribe();
   }
 })
 .catch(function(error) {
   console.log('Error unsubscribing', error);
 })
 .then(function() {
   updateSubscriptionOnServer(null);

   console.log('User is unsubscribed.');
   isSubscribed = false;

   updateBtn();
 });
}

function updateSubscriptionOnServer(subscription) {
 if (subscription) {
   console.log(JSON.stringify(subscription));
 }
}

let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
   e.preventDefault();
   deferredPrompt = e;
deferredPrompt.prompt();
});

While writing this, I just realized this might be redundant and I’m using the main.js code again in the javascript to force the service worker registration. Nonetheless this works perfectly for me.

Do take into account that for PWA support to work the app must comply with the following criteria (taken from https://web.dev/install-criteria/)

"In Chrome, your Progressive Web App must meet the following criteria before it will fire the beforeinstallprompt event and show the in-browser install promotion:

  • The web app is not already installed
  • Meets a user engagement heuristic ( In practice it means that you need to use the website for a certain amount of time and then the install prompt will appear . Usually it takes a few seconds. Or have interacted with the app previously.)
  • Be served over HTTPS
  • Includes a web app manifest that includes:
    • short_name or name
    • icons - must include a 192px and a 512px icon
    • start_url
    • display - must be one of fullscreen, standalone, or minimal-ui
    • prefer_related_applications must not be present, or be false
  • Registers a service worker with a fetch handler"

iOS specifics:

  • I use the iOS appearance settings in the “Settings” - “General” tab.
    image
  • iOS does not support Push notifications, so take that into account.

Since iOS doesn’t support PWA, you will not get the install prompt you get in Chrome for Android/PC/MacOs, you could show an install prompt on page load, by adding the following code to the “script in the body” field in “SEO / metatags”, or could incorporate it in a “run javascript” workflow action if you want the prompt to show under specific circumstances. Feel free to edit the code to fit your needs. (Prompts are in spanish)

<div class="install-popup" style="display:none;">
	<div class="install-popup-card">
		<div class="install-popup-close">&times;</div>
		<img class="install-popup-icon" src="" width="100" height="100"/>
		<div class="install-popup-message">
			Agrega esta aplicación a tu pantalla de inicio, es más fácil y rápido.
		</div>
		<div class="install-popup-prompt">
			Presiona 
			<svg class="install-icon-ios" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
				 width="168px" height="166px" viewBox="0 0 168 166" enable-background="new 0 0 168 166" xml:space="preserve">
			<g>
				<polygon fill="#0075FF" points="101,54 101,59 137,59 137,159 37,159 37,59 75,59 75,54 33,54 33,163 142,163 142,54 	"/>
				<polygon fill="#0075FF" points="84,14.625 84,99 91,99 91,14.619 114.022,37.115 118.648,33.122 87.632,2.043 56.524,33.122 
					61.032,37.104 	"/>
			</g>
			</svg>
			<svg class="install-icon-android" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 18c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm0-9c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm0-9c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3z"/></svg>
			 y después "Agregar a pantalla de inicio"
		</div>
	</div>
</div>
<style>
.install-popup{
	position: fixed;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	background-color: rgba(0,0,0,0.55);
	z-index: 9;
}
.install-popup-card{
	background-color: #fff;
	width: 86%;
	margin: 20% auto;
	padding: 14px;
	text-align: center;
	position: relative;
	border-radius: 2px;
}
.install-popup-close{
	font-size: 32px;
	color: #fff;
	position: absolute;
	top: -36px;
	right: 0;
}
.install-popup-icon{
	width: 100px;
	height: 100px;
	margin-top: 20px;
}
.install-popup-message{
	margin: 20px 0;
}
.install-popup-prompt{
	font-size: 12px;
}
.install-icon-ios{
	width: 24px;
	height: 24px;
	vertical-align: sub;
}
.install-icon-android{
	width: 24px;
	height: 24px;
	vertical-align: middle;
}
</style>
<script>
jQuery(function(){
	window.standalone = navigator.standalone || 
			/standalone=true/.test(window.location.href) || 
			(screen.height - document.documentElement.clientHeight < 75);

	var is = {
		iPhone: /iphone/i.test(navigator.userAgent),
		iPad: /ipad/i.test(navigator.userAgent),
		android: /android/i.test(navigator.userAgent)
	};

	var iosIcon = $('[rel="apple-touch-icon"]').prop("href"),
		icon = iosIcon || $('[rel="icon"]').prop("href"),
		$popup = $(".install-popup"),
		$icon = $popup.find(".install-popup-icon"),
		$close = $popup.find(".install-popup-close");

	if (is.iPhone || is.iPad){
		$(".install-icon-android").hide();
	}else{
		$(".install-icon-ios").hide();
	}

	$icon.prop("src", icon);
	$close.on("click", function(e){
		$popup.hide();
	});

	function showInstallPopup(){
		$popup.show();
	}

	if(is.iPhone || is.iPad){
		if (!window.standalone){
			// not installed, prompt
			showInstallPopup();
		}
	}

});
</script> 

For the rest of OneSignal notifications setup, refer to bubble’s instructions in the OneSignal Manual

I also used google developers documentation for push notification setup - Add push notifications to a web app  |  Google Codelabs

Lighthouse for PWA validation Overview  |  Lighthouse  |  Chrome for Developers

Hope this works for all of you, feel free to contribute!

43 Likes

kudos @aestela :clap: :clap: :clap:

5 Likes

@aestela great write up, will add it to my trello to-do list :+1:

1 Like

Gigantic writeup! Thank you aestela!

  1. Can you share one of those pwa apps URL to test it out ?
  2. How much is OneSignal?
  3. Is there some heavy lifting to be done with this plugin?

I’m asking because I’m on the free trial of progressier.com that also offers push notifications. But the installation process is a breeze. Maybe 5 min. However, progressier is 15 dol/month, so could be a dealbreaker for many. In my case it’s worth it for client apps, but for side apps / testing, i will use a free implementation of PWA and this guide looks super dupper.

Hi @lucas.ar, unfortunately all my apps have been developed for private companies and can’t share those URLs publicly, I’m working on a public version which, when ready, I’ll be more than happy to share.

Regarding OneSignal for my requirements, its completely free, I created 1 template and pass everything through the API workflows, it all depends on the scale of your app, but 10.000 recipients is more than enough for me right now.

On the PWA plugin, I haven’t used it, all I see it does is build your manifest file + push sounds/vibration in phone, without service-workers you won’t get real PWA support, Android Installation won’t be possible and adding it to home screen would add an Icon with a small Chrome logo above it. In the end, for my use case, what I want to achieve is this kind of behaviour:

image

InkedWhatsApp Image 2021-10-13 at 9.31.56 AM

Receiving notifications will make the phone vibrate and sound as with any normal app notification.

Although this setup isn’t as straightforward as using progressier, its a do it once and forget it, I wouldn’t pay $15/month for this unless you need the analytics and campaign generation (which could also be done with OneSignal). In the end it all depends on your use cases and if the approach is enough for you.

6 Likes

Hey!! Thanks for this well-explained guide on how to do it!

I was looking for this for a while already! :slight_smile:

1 Like

what can you get with progressier?

Hi @Makks basically they do it for you, I haven’t looked into it since this post was published though.

1 Like

Hey everyone,

I did the steps mentionned above, in Microsoft Edge i have the + button to install my app.

In chrome/Firefox nothing is displayed… How can I do to have it working please ?
I can see the forum of Bubble works also as a PWA :smiley:

Thank you

Hi @agence.webinti usually for Chrome, you have to interact with the website and or visit a couple of pages for the browser to prompt the install option.

Hello,

Thank you for your reply.
I could figured out the problem. Now it is working great on Chrome.
In firefox still not apparently but no matter

Thank you

Thanks for the guidance, @aestela. I have not yet had success using this on an iphone with safari or chrome. Should this work on an iphone? What does the user experience look like on the iphone?

Thank you so much.

Cheers

Hi! iOS doesn’t support PWA per se, for iOS you must use Safari and “Add to home screen” prompt described here:

Hello - thank you for the clear instructions - I have had some success. My app is installable (chrome shows the option to install in the address bar) but I cannot trigger a prompt. My beforeinstallevent fires, and I can save it. However, I cannot call a .prompt() on it.

I have tried the following:

  1. Checking the type of the event by logging it to the console:
    image
    The type logged is beforeinstallprompt:
    image

  2. Checking if the event is instanceof BeforeInstallPromptEvent:
    image
    The bool logged is false!:
    image

It is around there that I start pulling my hair out.

The error I get when I try doing event.prompt is ‘event.prompt is not a function’.

Might you be able to point me in the right direction?

1 Like

After messing with it for a while, I have figured it out. The issue is that jQuery (which is what I was using in my script) doesn’t return the original event generated by the browser. It wraps it up. The original event is to be found as a originalEvent property of the returned event. Instead of saving the event returned, save event.originalEvent. Everything works after that.

1 Like

Hey @hisFancy I was just about to answer that in “normal” conditions, chrome automatically generates its own prompt, but since you are using jQuery this might make sense. Glad you could figure it out!

1 Like

Thanks a lot for taking the time!

Hey, Thank you so much for this amazing work and tuto.

However, I’ve followed the tuto to set up the PWA. It works well and I got the message.
But I got a problem afterwards. I got the message “We just updated this app…” at all time, even after refreshing the app many time or deploying again .

Anyone in the same situation ?
Do you have any idea @aestela ?

Thank you !

Hi Quentin! Unfortunately I haven’t experienced this before.

Hi @aestela, this worked a charm and was super helpful! I’m having a couple of problems and not sure if you can help.

  1. On an iPhone, if I open the web app, I see the “Add to Home Screen” prompt. However, if I follow the instructions immediately to Add to Home Screen, for some reason the icon that shows up is a screenshot of the current page, and my iPhone “Add to Home Screen” icon I loaded in General Settings only shows up on the preview after a few seconds. This means that if the user is quick and adds immediately, they’ll get a very ugly icon on their home screen. Do you know what might be the issue?
  2. I’ve had a friend try this on a Samsung, and even after a few seconds of scrolling around - no prompt comes up for them to install. Is there a solution around this?

Thanks so much!