How to get a PWA to prompt you to install your website as an app
Progressive Web ApplicationsGetting Laravel up and running as an PWA application isn't tat hard. The challenge is rather what exactly do you want to do with said PWA? Here are some choices:
- Have sexy icons
- Send notifications
- Install itself as an app
- Do offline stuff
- Online/offline sync
Since doing offline stuff is a bit of a rabbit hole, we're going to focus on install itself as an app. During the journey to get this working, we'll actually cover the entire lifecycle of getting a Laravel application up and running with PWA.
Note: Apple has bastardised PWAs in favour of their monopoly. If you need advice on getting this working on iPhone, see here:
- https://stackoverflow.com/questions/51160348/pwa-how-to-programmatically-trigger-add-to-homescreen-on-ios-safari
- https://www.netguru.com/blog/pwa-ios
Skipping some terminology and catchphrases, getting an PWA up and running means getting a Javascript service working running when a page is loaded, ie. your home page. You could of course to this on another page, but for most people the home page is it.
When we started this journey we found many articles referencing this repository: https://github.com/silviolleite/laravel-pwa
Most of this article's beginning is about reverse engineering that repo. The repo is great, but don't fool yourself. Actually the repo does very little, and the author is quite clear what is does in the readme. My opinion is you can skip a lot in the repo or install the repo and then roll your own, because mostly you would want control. Also, remember PWA programming involves a lot of Javascript so at the end of the day the repo just assist with the boilerplate and you'll be doing more Javascript than anything else.
Continuing, here is the most important bit of boilerplate you'll need to get started, namely the head section:
@include('pwa') lang-php
What are we including? Here is it:
@php($config = config('pwa.manifest')) <!-- Web Application Manifest --> <link rel="manifest" href="{{ route('pwa.manifest') }}"> <!-- Chrome for Android theme color --> <meta name="theme-color" content="{{ $config['theme_color'] }}"> <!-- Add to homescreen for Chrome on Android --> <meta name="mobile-web-app-capable" content="{{ $config['display'] == 'standalone' ? 'yes' : 'no' }}"> <meta name="application-name" content="{{ $config['short_name'] }}"> <link rel="icon" sizes="{{ data_get(end($config['icons']), 'sizes') }}" href="{{ data_get(end($config['icons']), 'src') }}"> <!-- Add to homescreen for Safari on iOS --> <meta name="apple-mobile-web-app-capable" content="{{ $config['display'] == 'standalone' ? 'yes' : 'no' }}"> <meta name="apple-mobile-web-app-status-bar-style" content="{{ $config['status_bar'] }}"> <meta name="apple-mobile-web-app-title" content="{{ $config['short_name'] }}"> <link rel="apple-touch-icon" href="{{ data_get(end($config['icons']), 'src') }}"> <link href="{{ $config['splash']['640x1136'] }}" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> <link href="{{ $config['splash']['750x1334'] }}" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> <link href="{{ $config['splash']['1242x2208'] }}" media="(device-width: 621px) and (device-height: 1104px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" /> <link href="{{ $config['splash']['1125x2436'] }}" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" /> <link href="{{ $config['splash']['828x1792'] }}" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> <link href="{{ $config['splash']['1242x2688'] }}" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" /> <link href="{{ $config['splash']['1536x2048'] }}" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> <link href="{{ $config['splash']['1668x2224'] }}" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> <link href="{{ $config['splash']['1668x2388'] }}" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> <link href="{{ $config['splash']['2048x2732'] }}" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" /> <!-- Tile for Win8 --> <meta name="msapplication-TileColor" content="{{ $config['background_color'] }}"> <meta name="msapplication-TileImage" content="{{ data_get(end($config['icons']), 'src') }}"> <script type="text/javascript"> // Initialize the service worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/serviceworker.js', { scope: '.' }).then(function (registration) { // Registration was successful console.log('ServiceWorker registration successful with scope: ', registration.scope); console.log("Online status: " + navigator.onLine); }, function (err) { // registration failed :( console.log('ServiceWorker registration failed: ', err); console.log("Online status: " + navigator.onLine); }); } </script> lang-html
Note the extra bit of Javascript console output added:
console.log("Online status: " + navigator.onLine);
Next let's understand the `pwa.manifest` config and `pwa.manifest` routes:
<?php return [ 'name' => 'PWA Name Placeholder in config', 'manifest' => [ 'name' => env('APP_NAME', 'Progressive Web App'), 'short_name' => 'Vitacare', 'start_url' => '/', 'background_color' => '#ffffff', 'theme_color' => '#000000', 'display' => 'standalone', 'orientation'=> 'any', 'status_bar'=> 'black', 'icons' => [ '72x72' => [ 'path' => '/images/icons/icon-72x72.png', 'purpose' => 'any' ], '96x96' => [ 'path' => '/images/icons/icon-96x96.png', 'purpose' => 'any' ], '128x128' => [ 'path' => '/images/icons/icon-128x128.png', 'purpose' => 'any' ], '144x144' => [ 'path' => '/images/icons/icon-144x144.png', 'purpose' => 'any' ], '152x152' => [ 'path' => '/images/icons/icon-152x152.png', 'purpose' => 'any' ], '192x192' => [ 'path' => '/images/icons/icon-192x192.png', 'purpose' => 'any' ], '384x384' => [ 'path' => '/images/icons/icon-384x384.png', 'purpose' => 'any' ], '512x512' => [ 'path' => '/images/icons/icon-512x512.png', 'purpose' => 'any' ], ], 'splash' => [ '640x1136' => '/images/icons/splash-640x1136.png', '750x1334' => '/images/icons/splash-750x1334.png', '828x1792' => '/images/icons/splash-828x1792.png', '1125x2436' => '/images/icons/splash-1125x2436.png', '1242x2208' => '/images/icons/splash-1242x2208.png', '1242x2688' => '/images/icons/splash-1242x2688.png', '1536x2048' => '/images/icons/splash-1536x2048.png', '1668x2224' => '/images/icons/splash-1668x2224.png', '1668x2388' => '/images/icons/splash-1668x2388.png', '2048x2732' => '/images/icons/splash-2048x2732.png', ], 'shortcuts' => [ [ 'name' => 'Shortcut Link 1', 'description' => 'Shortcut Link 1 Description', 'url' => '/shortcutlink1', 'icons' => [ "src" => "/images/icons/icon-72x72.png", "purpose" => "any" ] ], [ 'name' => 'Shortcut Link 2', 'description' => 'Shortcut Link 2 Description', 'url' => '/shortcutlink2' ] ], 'custom' => [] ] ]; lang-php
The PWA routes look like this:
<?php Route::group(['as' => 'pwa.'], function() { Route::get('/manifest.json', 'PWAController@manifestJson') ->name('manifest'); Route::get('/offline/', 'PWAController@offline'); }); lang-php
So far most items are entirely self explanatory.
We'll cover controller methods, because what we really want to do is get our hands dirty on some Javascript. So here is the key file you'll be spending time on:
It's copied to /public by the repository:
var staticCacheName = "pwa-v" + new Date().getTime(); var filesToCache = [ '/offline', '/css/app.css', '/js/app.js', '/images/icons/icon-72x72.png', '/images/icons/icon-96x96.png', '/images/icons/icon-128x128.png', '/images/icons/icon-144x144.png', '/images/icons/icon-152x152.png', '/images/icons/icon-192x192.png', '/images/icons/icon-384x384.png', '/images/icons/icon-512x512.png', ]; // Cache on install self.addEventListener("install", event => { this.skipWaiting(); event.waitUntil( caches.open(staticCacheName) .then(cache => { return cache.addAll(filesToCache); }) ) }); // Clear cache on activate self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(cacheName => (cacheName.startsWith("pwa-"))) .filter(cacheName => (cacheName !== staticCacheName)) .map(cacheName => caches.delete(cacheName)) ); }) ); }); // Serve from Cache self.addEventListener("fetch", event => { event.respondWith( caches.match(event.request) .then(response => { return response || fetch(event.request); }) .catch(() => { return caches.match('offline'); }) ) }); lang-js
Great! Now we have a foundation and can move to the actual step of asking about installation.
Once the app is installed, you might want to uninstall it. To do so, we followed this article:
https://support.google.com/chrome/answer/9658361
- On your computer, open Chrome.
- Go to a website you want to uninstall.
- At the top right, click More Uninstall [app name].
- Click Remove.
- To delete app data from Chrome, select "Also delete data from Chrome."
The problem was then that the app never wanted to install again.
You can see all Chrome apps like this:
chrome://apps
But it was gone. Advice was to clear cookies, cache, and history, but this seemed radical to me.
Next problem is where is the install icon?
https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/How_to/Define_app_icons
Commenting out this work on Chrome, Mac:
// If you comment out this icon, on Chrome, Mac, Storm's icon will be used '152x152' => [ 'path' => '/images/icons/pwa/icon-152x152.png', 'purpose' => 'any' ], lang-php
References:
The official documentation is pretty good:
https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable
What nice about the official docs is it mentions the various stores for Android and Apple and guides how to get your app listed.
https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeinstallprompt_event
Here is more about icons:
https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/How_to/Define_app_icons