pwa - prompt add to home screen dosen't show - javascript

I develop a web application with symfony 4.3, then I add pwa features to it. I test with lighthouse extension in chrome and this is result:
Now problem is prompt for add icon to home screen dosen't show and I have this error :
Uncaught TypeError: Cannot read property 'prompt' of undefined
code js:
var deferredPrompt ;
var btnAdd = document.getElementById('butInstall') ;
function launchPromptPwa(){
var deferredPrompt;
btnAdd = document.getElementById('butInstall') ;
window.addEventListener('beforeinstallprompt', (e) => {
console.log('0');
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e;
btnAdd.style.display = block;
showAddToHomeScreen();
});
btnAdd.addEventListener('click', (e) => {
console.log('1');
//btnAdd.style.display = 'none';
//Show the prompt
deferredPrompt.prompt();
// Wait for the user to respond to the prompt
deferredPrompt.userChoice
.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
} else {
console.log('User dismissed the A2HS prompt');
}
deferredPrompt = null;
});
});
window.addEventListener('appinstalled', (evt) => {
console.log('a2hs installed');
});
if (window.matchMedia('(display-mode: standalone)').matches) {
console.log('display-mode is standalone');
}
}
I test for the display prompt in chrome.

To avoid the error you can test first whether the deferredPrompt variable is initialised and skip the code logic if undefined:
if (deferredPrompt) {
deferredPrompt.prompt();
// ...
}
Then, is the beforeinstallprompt event triggered?
If so, you have to proof if the event object is defined, as you use it to initialise your variable:
deferredPrompt = e;
Keep in mind that you need a running service worker in order to let the beforeinstallprompt event being triggered. And the service worker needs a secure connection (https) or running localhost and served via web server.
You can open Chrome Dev Tools (F12) and access the "Application" tab to verify that a web manifest is correctly set and a service worker is installed.
I wrote some articles about service workers, caching strategies and PWAs if you are interested in deepening the topic.
UPDATE
If you want to serve content offline, you have to implement caching strategies for your service worker (eg. Stale while revalidate). Following the link above you can learn about different strategies and how you can implement them.
When you implement caching strategies, all the static assets (like css or js files) or data requests will be intercept by the service worker and if there is a match with the given rules, it will cache them or provide them from the cache. Since the cache is on the client side, those resources are available also offline.
As example, to cache static assets:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll(
[
'/css/bootstrap.css',
'/css/main.css',
'/js/bootstrap.min.js',
'/js/jquery.min.js',
'/offline.html'
// Add anything else you need to be cached during the SW install
]
);
})
);
});

Related

How to make PWA detect any HTML changes and update it automatically?

Essentially I'm wondering what to change in my sw.js file so that if I add a new HTML element, when the user opens up the PWA it takes a moment to update and refresh itself and display the new content as well?
Currently when I'm connected to the Wifi, the PWA does not make any changes, and when I disconnect my Wifi, it only caches my HTML file and gets rid of my CSS. So I guess my follow up question is how do I make the other pages cache as well so that they're available offline? (My app is only 200kb).
Here is my sw.js code:
// On install - caching the application shell
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('sw-cache').then(function(cache) {
// cache any static files that make up the application shell
return cache.add('index.html');
})
);
});
// On network request
self.addEventListener('fetch', function(event) {
event.respondWith(
// Try the cache
caches.match(event.request).then(function(response) {
//If response found return it, else fetch again
return response || fetch(event.request);
})
);
});

Blazor - Service worker not installing due to integrity check failure

I'm trying to setup PWA for my blazor application. I followed the instructions on: https://learn.microsoft.com/en-us/aspnet/core/blazor/progressive-web-app?view=aspnetcore-6.0&tabs=visual-studio
But when I open the deployed website the following error occurs:
Failed to find a valid digest in the 'integrity' attribute for resource 'domain/manifest.json' with computed SHA-256 integrity 'uDWnAIEnaz9hFx7aEpJJVS1a+QB/W7fMELDfHWSOFkQ='. The resource has been blocked.
Unknown error occurred while trying to verify integrity.
service-worker.js:22
Uncaught (in promise) TypeError: Failed to fetch
at service-worker.js:22:54
at async onInstall (service-worker.js:22:5)
In the source file this happens here:
async function onInstall(event) {
console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
I think the error is happening since the entry in assetsRequests has a wrong hash and the resource is blocked. If I remove the file from the service-worker-assets.js, the service worker installs and the PWA can be used. But I think this is not a reliable solution.
This also happens sometimes for the appsettings.json. In the service-worker-assets.js I can find the following entry:
{
"hash": "sha256-+Py0\/ezc+0k1sm\/aruGPrVhS1jOCTfPKMhOSS+bunek=",
"url": "manifest.json"
},
So the hash does not seem to match. Where does the browser take the wrong hash from? How can I fix this so it does match?
Also it seems that the app is caching older files sometimes. Even when I do a "Reset Cache & Hard Reload" in Chrome the service-worker.js file is still an older version. Any idea how to fix this as well, since it might be related?
Edit: I was also checking this solution: https://stackoverflow.com/a/69935118/11385442. But in the mentioned blazor.boot.json I cannot find any reference to the manifest.json or the appsettings.json. Only Dlls are listed. So the problem only seems to relate to files not listed in blazor.boot.json.
Edit2: What I can see on the webserver is that the following files are published:
appsettings.json
appsettings.json.br
appsettings.json.gzip
So it seems like compressed version are added. Also the appsettings.json has a different size than the one in the solution. My guess is that somewhere in the build or release pipeline (Azure) the files are modified. But even when I copy the appsettings.json manually to the webserver the error still occurs. I was following Information provided here: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly?view=aspnetcore-5.0
(Diagnosing integrity problems)
My guess was right. The appsettings.json was modified probably due to the xml transformation in the azure pipeline. My current solution is to exclude integrity validation for such resources as described in the following answer: Error loading appsettings.Production.json due to digest integrity issue
Also I changed the "sw-registrator.js" mentioned in the original posts comments to work correctly, because it didn't load the new files into the cache:
function invokeServiceWorkerUpdateFlow(registration) {
if (confirm("New version available, reload?") == true) {
if (registration.waiting) {
console.info(`Service worker registrator: Post skip_waiting...`);
// let waiting Service Worker know it should became active
registration.waiting.postMessage('SKIP_WAITING')
}
}
}
function checkServiceWorkerUpdate(registration) {
setInterval(() => {
console.info(`Service worker registrator: Checking for update... (scope: ${registration.scope})`);
registration.update();
}, 60 * 1000); // 60000ms -> check each minute
}
// check if the browser supports serviceWorker at all
if ('serviceWorker' in navigator) {
// wait for the page to load
window.addEventListener('load', async () => {
// register the service worker from the file specified
const registration = await navigator.serviceWorker.register('/service-worker.js');
// ensure the case when the updatefound event was missed is also handled
// by re-invoking the prompt when there's a waiting Service Worker
if (registration.waiting) {
invokeServiceWorkerUpdateFlow(registration);
}
// detect Service Worker update available and wait for it to become installed
registration.addEventListener('updatefound', () => {
if (registration.installing) {
// wait until the new Service worker is actually installed (ready to take over)
registration.installing.addEventListener('statechange', () => {
if (registration.waiting) {
// if there's an existing controller (previous Service Worker), show the prompt
if (navigator.serviceWorker.controller) {
invokeServiceWorkerUpdateFlow(registration);
} else {
// otherwise it's the first install, nothing to do
console.log('Service worker registrator: Initialized for the first time.')
}
}
});
}
});
checkServiceWorkerUpdate(registration);
let refreshing = false;
// detect controller change and refresh the page
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.info(`Service worker registrator: Refreshing app... (refreshing: ${refreshing})`);
if (!refreshing) {
window.location.reload();
refreshing = true
}
});
});
}
else
{
console.error(`Service worker registrator: This browser doesn't support service workers.`);
}
Also I had to add this in service-worker.js:
self.addEventListener('message', (event) => {
console.info('Service worker: Message received');
if (event.data === 'SKIP_WAITING') {
// Cause the service worker to update
self.skipWaiting();
}
});
This code was mostly taken from https://whatwebcando.today/articles/handling-service-worker-updates/

How to solve service worker navigate "This service worker is not the client's active service worker" error?

I've created a service worker for my quasar pwa to manage fcm web background notifications.
The purpose is to manage click on foreground and background notifications and redirect user to specific page of my pwa.
So when I get a notification I have two scenario:
foreground notifications:
1a) the browser is focused/maximized + the user is already on the pwa tab + the pwa is on the right page -> nothing to do
1b) the browser is focused/maximized + the user is already on the pwa tab + the pwa is on another page -> I have to redirect to the specific pwa page
background notifications:
2a) the browser is not focused or minimized or the user is not on the pwa tab + the pwa is on the right page -> I have to focus/maximize the browser or focus the pwa tab and then nothing else to do
2b) the browser is not focused or minimized or the user is not on the pwa tab + the pwa is on another page -> I have to focus/maximize the browser or focus the pwa tab and then redirect to the specific pwa page
Everything works fine in 1a, 1b and 2a. For 2b I get this weird error "This service worker is not the client's active service worker".
I have the following code in service worker to manage redirect on background notification click. And I get the error on the navigate() method.
self.addEventListener('notificationclick', function(event) {
console.log('notificationclick', event);
event.notification.close();
let route = event.notification.data.route ? JSON.parse(event.notification.data.route) : null;
if(route && route.params && route.params.token) {
const domain = route.domain;
const path = '/#/' + route.name + '/' + route.params.token;
const fullUrl = domain + path
event.waitUntil(clients.claim().then(() => {
return clients.matchAll({
type: 'window',
includeUncontrolled: true
})
.then(clients => clients.filter(client => client.url.indexOf(domain) !== -1))
.then(matchingClients => {
if (matchingClients[0]) {
return matchingClients[0].focus().then(function (client) {
client.navigate(path)
.then(client => {
}).catch(function (e) {
console.log(e); --> here I get the error
});
}).catch(function (e) {
console.log(e);
});
}
return clients.openWindow(fullUrl);
})
})
);
}
});
I searched for this error on the web but didn't find any reference so I don't understand the error and cannot solve it. Anybody can help please?
Thanks
I also couldn't get it working with client.navigate(path) no matter what I tried... and after several hours of searching and going down deep rabbit holes in GitHub, MDN docs, etc. I just ended up using the client.postMessage() interface.
In your code above replace client.navigate(path) with the following:
client.postMessage({
action: 'redirect-from-notificationclick',
url: path,
})
And then in your app code listen to this message:
// Listen to service worker messages sent via postMessage()
navigator.serviceWorker.addEventListener('message', (event) => {
if (!event.data.action) {
return
}
switch (event.data.action) {
case 'redirect-from-notificationclick':
window.location.href = event.data.url
break
// no default
}
})

detect offline or online using service worker

I am using service worker to check if a user is online or offline when a request is made. Here are some of the approaches I have taken:
In this method in service worker,
self.addEventListener("fetch", (event) => {
if (navigator.onLine){
}
})
navigator.onLine only works when you check/uncheck the offline checkbox in Inspect Element. But when I switch the device's internet on/off, it will always return true whether im offline or online.
And also from what i've seen in other answers, navigator.onLine will return true if you are connected to your local network even if your local network has no internet connection.
I have tried to ping a url in the self.addEventListener("fetch", {...}) method as shown here https://stackoverflow.com/a/24378589/6756827. This will bring an error in in the new XMLHttpRequest(); object.
I have tried to load an online resource (an image) in the self.addEventListener("fetch", {...}) method as shown here https://stackoverflow.com/a/29823818/6756827. The console shows an error for new Image();.
Because none of these approaches work, how do we check if a user is online or offline in service worker when a request is made ?
In the service worker you do not have access to the navigator object and thus no access to the online property or events.
Instead you know you are offline when a fetch throws an exception. The only time a fetch will throw an exception is when the device cannot connect to the Internet/Network.
self.addEventListener( "fetch", function ( event ) {
event.respondWith(
handleRequest( event )
.catch( err => {
//assume offline as everything else should be handled
return caches.match( doc_fallback, {
ignoreSearch: true
} );
} )
);
} );
For a more complex handling of the situation you can sync messages of online/offline state with the UI using the message API:
self.addEventListener( "message", event => {
//handle message here
}

JavaScript (PWA) - What is the correct way to initialize service-worker?

What is the better way to initialize service-worker used for PWA caching?
Method 1:
I expect that in this case will be installed new SW instance. I guess that's not necessary and I want to re-activate existing SW already running in the browser - am I right?
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js').then(registration => {
// Registration success
}, function(err) {
// Registration failed
});
});
}
Method 2: In this case I'm checking that there SW already registered and I'm installing registering a new one only if there is non registered.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.getRegistrations().then(registrations => {
const isServiceWorkerNotRegistered = registrations.length === 0;
if (isServiceWorkerNotRegistered) {
navigator.serviceWorker.register('sw.js').then(registration => {
// Registration success
}).catch(error => {
// Registration failed
});
} else {
console.log('Service worker already registered.');
}
});
});
}
On top of that... Is it really necessary to wait for window to load (line 2)? Since SW is running asynchronously in the browser I guess I don't need the app to be completely loaded.
Waiting for the load event is so that assets being downloaded by the service worker don't take bandwidth and device resources from rendering the currently requested page. See improving the boilerplate.
For your registration question, use the simple first version.
Unless you change the URL of the service worker script, navigator.serviceWorker.register() is effectively a no-op during subsequent visits.
subsequent visits
If the service worker is not registered, register will register it. If a service worker is already registered, register will basically not do anything.

Categories

Resources