ReactJs PWA not updating on iOS - ios

I'm building a ReactJs PWA but I'm having trouble detecting updates on iOS.
On Android everything is working great so I'm wondering if all of this is related to iOS support for PWAs or if my implementation of the service worker is not good.
Here's what I've done so far:
Build process and hosting
My app is built using webpack and hosted on AWS. Most of the files (js/css) are built with some hash in their name, generated from their content. For those which aren't (app manifest, index.html, sw.js), I made sure that AWS serves them with some Cache-Control headers preventing any cache. Everything is served over https.
Service Worker
I kept this one as simple as possible : I didn't add any cache rules except precache for my app-shell:
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
Service-worker registration
Registration of the service worker occurs in the main ReactJs App component, in the componentDidMount() lifecycle hook:
componentDidMount() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
reg.onupdatefound = () => {
this.newWorker = reg.installing;
this.newWorker.onstatechange = () => {
if (this.newWorker.state === 'installed') {
if (reg.active) {
// a version of the SW is already up and running
/*
code omitted: displays a snackbar to the user to manually trigger
activation of the new SW. This will be done by calling skipWaiting()
then reloading the page
*/
} else {
// first service worker registration, do nothing
}
}
};
};
});
}
}
Service worker lifecycle management
According to the Google documentation about service workers, a new version of the service worker should be detected when navigating to an in-scope page. But as a single-page application, there is no hard navigation happening once the app has been loaded.
The workaround I found for this is to hook into react-router and listen for route changes, then manually ask the registered service worker to update itself :
const history = createBrowserHistory(); // from 'history' node package
history.listen(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.getRegistration()
.then((reg) => {
if (!reg) {
return null;
}
reg.update();
});
}
});
Actual behavior
Throwing a bunch of alert() everywhere in the code showed above, this is what I observe :
When opening the pwa for the first time after adding it to the homescreen, the service worker is registered as expected, on Android and iOS
While keeping the app opened, I deploy a new version on AWS. Navigating in the app triggers the manual update thanks to my history listener. The new version is found, installed in the background. Then my snackbar is displayed and I can trigger the switch to the new SW.
Now I close the app and deploy a new version on AWS. When opening the app again :
On Android the update is found immediately as Android reloads the page
iOS does not, so I need to navigate within the app for my history listener to trigger the search for an update. When doing so, the update is found
After this, for both OS, my snackbar is displayed and I can trigger the switch to the new SW
Now I close the app and turn off the phones. After deploying a new version, I start them again and open the app :
On Android, just like before, the page is reloaded which detects the update, then the snackbar is displayed, etc..
On iOS, I navigate within the app and my listener triggers the search for an update. But this time, the new version is never found and my onupdatefound event handler is never triggered
Reading this post on Medium from Maximiliano Firtman, it seems that iOS 12.2 has brought a new lifecycle for PWAs. According to him, when the app stays idle for a long time or during a reboot of the device, the app state is killed, as well as the page.
I'm wondering if this could be the root cause of my problem here, but I was not able to find anyone having the same trouble so far.

So after a lot of digging and investigation, I finally found out what was my problem.
From what I was able to observe, I think there is a little difference in the way Android and iOS handle PWAs lifecycle, as well as service workers.
On Android, when starting the app after a reboot, it looks like starting the app and searching an update of the service worker (thanks to the hard navigation occuring when reloading the page) are 2 tasks done in parallel. By doing that, the app have enough time to subscribe to the already existing service worker and define a onupdatefound() handler before the new version of the service worker is found.
On the other hand with iOS, it seems that when you start the app after a reboot of the device (or after not using it for a long period, see Medium article linked in the main topic), iOS triggers the search for an update before starting your app. And if an update is found, it will be installed and and enter its 'waiting' status before the app is actually started. This is probably what happens when the splashscreen is displayed...
So in the end, when your app finally starts and you subscribe to the already existing service worker to define your onupdatefound() handler, the update has already been installed and is waiting to take control of the clients.
So here is my final code to register the service worker :
componentDidMount() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
if (reg.waiting) {
// a new version is already waiting to take control
this.newWorker = reg.waiting;
/*
code omitted: displays a snackbar to the user to manually trigger
activation of the new SW. This will be done by calling skipWaiting()
then reloading the page
*/
}
// handler for updates occuring while the app is running, either actively or in the background
reg.onupdatefound = () => {
this.newWorker = reg.installing;
this.newWorker.onstatechange = () => {
if (this.newWorker.state === 'installed') {
if (reg.active) {
// a version of the SW already has control over the app
/*
same code omitted
*/
} else {
// very first service worker registration, do nothing
}
}
};
};
});
}
}
Note :
I also got rid of my listener on history that I used to trigger the search for an update on every route change, as it seemed overkill.
Now I rely on the Page Visibility API to trigger this search every time the app gets the focus :
// this function is called in the service worker registration promise, providing the ServiceWorkerRegistration instance
const registerPwaOpeningHandler = (reg) => {
let hidden;
let visibilityChange;
if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
hidden = 'hidden';
visibilityChange = 'visibilitychange';
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden';
visibilityChange = 'msvisibilitychange';
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden';
visibilityChange = 'webkitvisibilitychange';
}
window.document.addEventListener(visibilityChange, () => {
if (!document[hidden]) {
// manually force detection of a potential update when the pwa is opened
reg.update();
}
});
return reg;
};

As noted by Speckles (thanks for saving me the headache), iOS installs the new SW before launching the app. So the SW doesn't get a chance to catch the 'installing' state.
Work-around: check if the registration is in the waiting state then handle it.
I've made an (untested) example of handling this. - a mod to the default CRA SW.

Related

Sending commands to the web app in electron renderer process since contextIsolation changes

I understand that contextIsolation changes are introduced for security purposes, and i read about contextBridge which exposes custom API to the webapp running in the renderer process so web app can control electron app in which is runing.
electron preload script
const contextBridge = require("electron").contextBridge;
contextBridge.exposeInMainWorld("electronApi", {
'doSomething' : function () {
// some code to execute
}
});
web app
window.electronApi.doSomething();
This is perfectly clear and i understand why is this done this way.
However, i do not understand how can communication work the other way, so how can electron execute web app commands? Let's take following example, web app has window.someWebAppMethod defined and electron should execute it
web app
window.someWebAppMethod = function () {
// do somehing web app related
}
electron preload script
window.someWebAppMethod()
^^ this does not work because of contextIsolation which was whole point of contextIsolation, but i still need to have a certain way of triggering web app commands from electron. Most obvious reason is let's say i have electron main menu with command labeled "Open Quick Jump" which should tell the web app loaded in rendered process to execute method which will show the "Quick Jump" function of the web app.
Maybe i'm missing something painfully obvious, but i'd still appreciate any help i can get.
Thanks
I found a way of doing this. Not sure it's obvious or if it is secure enough, but here it is:
electron preload script
const contextBridge = require("electron").contextBridge;
let doSomethingInWebApp = null;
contextBridge.exposeInMainWorld("electronApi", {
'exposeDoSomethingInWebApp' : function (callback) {
doSomethingInWebApp = callback;
}
});
web app
if (window.electronApi && window.electronApi.exposeDoSomethingInWebApp) {
window.electronApi.exposeDoSomethingInWebApp(function () {
// execute whatever you need to execute in webapp
});
}
electron preload script
if (doSomethingInWebApp) {
// execute previously defined custom behavior in web app
doSomethingInWebApp();
}
So it's quite simple and it works.

Electron auto update fails silently when installing update on Windows

I have an electron app which uses electron-builder for building, packing & publishing the app.
I have the following auto-update code:
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = "info";
autoUpdater.autoDownload = true;
const updateCheck = () => {
autoUpdater.checkForUpdates().then(resp => {
log.info("autoUpdate response:");
log.info(resp);
});
};
app.on("ready", async () => {
log.info(`Version: ${app.getVersion()}`);
autoUpdater.on("update-downloaded", () => {
log.info("update downloaded");
setImmediate(() => {
try {
log.info("installing update");
// app.relaunch();
autoUpdater.quitAndInstall();
} catch (err) {
log.error("Error installing update");
log.error(err);
}
});
});
autoUpdater.on("error", err => {
log.error("AutoUpdater error");
log.error(err);
});
updateCheck();
schedule.scheduleJob("*/10 * * * *", updateCheck);
});
When I publish a new version, the auto-updater detects it, downloads it successfully, and then tries to install it.
During installation of the update, the progress bar fills up halfway, then disappears.
The app remains closed and does not automatically relaunch after the progress bar disappears.
When I re-launch it manually, it is still the old version. It detects that there is an update which has already downloaded, and tries to install it, with the same result (progress bar fills halfway then disappears, app remains closed after).
My log file shows no errors, either from the catch block or the autoUpdater.on("error") callback.
The location C:\Users\<User>\AppData\Local\<app-name>-updater has an installer.exe file which installs the previous version, and a pending folder which contains an installer for the new version. Manually executing this installer causes the app to be updated with no errors.
I tried testing using checkForUpdatesAndNotify instead of checkForUpdates (and commenting out the call to quitAndInstall), and it worked as advertised, however I would like to ensure the update is installed immediately rather than wait for the user to quit on their own.
How can I debug this further? Why am I not seeing any errors? Where should I be looking for them? What am I doing wrong?
The problem turned out to be the fact that I created the window with the option closable: false, preventing the auto updater from closing it. The auto updater works perfectly after removing this setting
Quick question: have you used
this answer to make your App window closed and minimizable to tray?
If you did, You should application.isQuiting = true before calling autoUpdater.quitAndInstall();
otherwise the application.close() function is pretty much blocked from anywhere other then the contextMenu.
I was facing the same issue and I did a lot of RnD. After trying for few days I found that there was no issue with the auto updater instead it was the issue with the file name I was uploading on my server. Make sure your exe file name for every new version is different otherwise it will not install after auto update.
For eg.
setup_0.0.1.exe than your next version file should be setup_0.0.2.exe
Make sure to update the file name in latest.yml and blockmap file as well.

How to find if electron app is in foreground?

I have a requirement where I want to perform an action inside the electron app only when it is in foreground.
It is an electron-react application. On mounting of a component, I want to schedule a periodic task which only runs when the app is in focus or is being used by the user. And pause the task when the app goes in background.
How can we detect the Electron app being in foreground?
You can use the isFocused method from BrowserWindow. To get your own BrowserWindow, you can do this :
remote.BrowserWindow.getAllWindows();
This will return all your app's windows. So to get the first / primary window, you could deconstruct the array like this :
const [yourBrowserWindow] = remote.BrowserWindow.getAllWindows();
console.log(yourBrowserWindow.isFocused());
You can use the focus / blur events on your BrowserWindow to be notified when the app is focused / unfocused.
mainWindow = new BrowserWindow({})
mainWindow.on('focus', () => {
console.log('window got focus')
})
mainWindow.on('blur', () => {
console.log('window blur')
})
You may want to update the component's state within these event handlers or use any other method to keep track of the current focus status.
This assumes that you have a single application window. If you have multiple, you'll need to extend the check to cover all of your windows.

Any way yet to auto-update (or just clear the cache on) a PWA on iOS?

I have been struggling on iOS with something that works easily on Android: Getting my PWA to auto-update when there is a new version. I am not at all sure this is even possible on iOS. I have used vue.js and Quasar to build my app, and everything works out of the box on Android. Here is the (ugly, terrible) way things stand currently on the iOS version:
I can check my own server for the version and compare it against the current one stored in my app (in indexedDB) and throw up a notice that there is a new version. So far so good.
Other than having the user MANUALLY CLEAR THE SAFARI CACHE (!!) there is no way I can figure out how to programmatically clear the PWA cache from within the app or force an upload in another way.
So at this point I guess my questions are:
Has ANYONE been able to get a PWA on iOS (11.3 or later) to auto-update when a new version is available?
Is there a way to clear the (safari) app cache from within my PWA?
Obviously it is an incredibly awful user experience to notify the user that in order to update they must perform several steps outside of the app to be able to refresh it, but it seems this is where iOS stands at the moment unless I am missing something. Has anyone anywhere made this work?
After weeks and weeks of searching, I finally found a solution:
I add a check for versionstring on the server, and return it to the app as mentioned above.
I look for it in localtstorage (IndexedDB) and if I don’t find it, I add it. If I do find it, I compare versions and if there is a newer one on the server, I throw up a dialog.
Dismissing this dialog (my button is labeled “update”) runs window.location.reload(true) and then stores the new versionstring in localstorage
Voila! My app is updated! I can't believe it came down to something this simple, I could not find a solution anywhere. Hope this helps someone else!
UPDATE SEPT 2019:
There were a few problems with the technique above, notably that it was bypassing the PWA service worker mechanisms and that sometimes reload would not immediately load new content (because the current SW would not release the page). I have now a new solution to this that seems to work on all platforms:
function forceSWupdate() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function (registrations) {
for (let registration of registrations) {
registration.update()
}
})
}
}
forceSWupdate()
And inside my serviceworker I now throw up the dialog if there is an update, and do my location.reload(true) from there. This always results in my app being refreshed immediately (with the important caveat that I have added skipWaiting and clientsClaim directives to my registration).
This works on every platform the same, and I can programatically check for the update or wait for the service worker to do it by itself (although the times it checks vary greatly by platform, device, and other unknowable factors. Usually not more than 24 hours though.)
If anyone is still having issues with this, registration.update() did not work for me. I used the exact solution but when the version from my server did not match my local stored version, I had to unregister the service workers for it to work.
if ('serviceWorker' in navigator) {
await this.setState({ loadingMessage: 'Updating Your Experience' })
navigator.serviceWorker.getRegistrations().then(function(registrations) {
registrations.map(r => {
r.unregister()
})
})
await AsyncStorage.setItem('appVersion', this.state.serverAppVersion)
window.location.reload(true)
}
Then when the app reloads, the service worker is reregistered and the current version of the app is visible on iOS safari browsers and 'bookmarked' PWAs.
Instead of prompting the user, that a new version is available, you can also extend the 'activate' Eventlistener to delete your old cache whenever you publish a new serviceworker version.
Add version and name variables
var version = "v3" // increase for new version
var staticCacheName = version + "_pwa-static";
var dynamicCacheName = version + "_pwa-dynamic";
Delete caches, when their names do not fit the current version:
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
if (!cacheName.startsWith(staticCacheName) &&
!cacheName.startsWith(dynamicCacheName)) {
return true;
}
}).map(function(cacheName) {
console.log('Removing old cache.', cacheName);
return caches.delete(cacheName);
})
);
})
);
});
(credits: https://stackoverflow.com/a/45468998/14678591)
In order to make this work for iOS safari browsers and 'bookmarked' PWAs too, I just added the sligthly reduced function by #jbone107:
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
if (!cacheName.startsWith(staticCacheName) &&
!cacheName.startsWith(dynamicCacheName)) {
return true;
}
}).map(function(cacheName) {
// completely deregister for ios to get changes too
console.log('deregistering Serviceworker')
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function(registrations) {
registrations.map(r => {
r.unregister()
})
})
window.location.reload(true)
}
console.log('Removing old cache.', cacheName);
return caches.delete(cacheName);
})
);
})
);
});
This way you just have to increase the version number and updating is done by the serviceworker automatically.

Appcelerator: Event when reopening App

I want my app to reload data when it will be reopened (from iOS "Multitasking").
I've tested:
Ti.UI.addEventListener('reload', function() {
alert('reloaded app');
});
but this event just gets fired when the App will be opened the first time.
The app entering the foreground is an app level event. So you need to register on Ti.App, not Ti.UI. In fact I can't find any reference to the event you are using.
Ti.App.addEventListener('resume', function() {
alert('reloaded app');
});
Or you can use "resumed" for after it has completely returned.
See this page

Resources