How to Share Cookie or State Between Progressive Web Application in Standalone Mode and Safari on iOS
Introduction
Recently, while working on a PWA project, I needed to share data between a browser and PWA added to home screen. As it might seem simple task on android, guess what - on iOS it is nearly impossible. Session, cookies, local storage, and even Service Worker instance is not shared between safari and standalone mode. So, even if you persist something in local storage - it won’t be available in another app instance. You may have noticed that behavior while trying out some PWAs on iOS - especially those with Facebook/Google authentication - they simply don’t work.
If you ever tried to create PWA for iOS, there is a huge chance that you’ve also had a similar requirement, as sharing data between instances is useful or required in some cases. Especially if you want to provide a good user experience in your app.
Most importantly it is also required to implement OAuth - Facebook, Google, etc. login - which I will cover in details in a separate article.
Solution
Obviously, there would not be an article if I haven’t found a workaround for that! Indeed, session, cookies, local storage, and even Service Worker instance is not shared between safari and standalone, and I think it is not possible to share them, but apparently cache content is the same for both instances! We can take advantage of that to create communication between our app's run in different modes, and even create a workaround for sharing cookies or state.
So the idea is to create a fake endpoint in service worker, which would save data in the cache on POST request and return cached data on GET request. We also have to make sure that our service worker is activated straight away - that will enable us to read/persist data as soon as our app is loaded.
Demo
You can check out the working example at SW data sharing demo.
Save a value in safari (fill-in input and click save), then add that website to the home screen, and hopefully, you will be able to read the saved value in standalone mode by clicking fetch.
Step 1: Prepare your service worker to share data
As I’ve mentioned in solution section, we would like to create a fake endpoint in a service worker, which will be only used to store data. We will achieve that using fetch event listener and Cache API.
Keep in mind that Cache API is designed to work on requests and responses, so you can’t simply put a string to it (you would get an error). That's why in my example I’m wrapping data into Request/Response.
It is also good to remember that you can read request body only once. If you want to learn more about Cache API take a look into Cache - Web APIs | MDN
While creating this script at first I did not use clients.claim(), and was wondering why requests are not handled by my service worker - it was activated and everything seems to be fine.
After googling around, I’ve found out that by default, a page's fetches won't go through a service worker unless the page request itself went through a service worker. So you would need to refresh the page to make our trick work. Thankfully there is a way to change that behavior with clients.claim() - it will activate our endpoint straightaway.
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});
So here is the final code which we would add to our service worker:
// our fake endpoint to store data
const SHARED_DATA_ENDPOINT = '/token';
// see: https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});
self.addEventListener('fetch', function(event) {
const {
request,
request: {
url,
method,
},
} = event;
if (url.match(SHARED_DATA_ENDPOINT)) {
if (method === 'POST') {
request.json().then(body => {
caches.open(SHARED_DATA_ENDPOINT).then(function(cache) {
cache.put(SHARED_DATA_ENDPOINT, new Response(JSON.stringify(body)));
});
});
return new Response('{}');
} else {
event.respondWith(
caches.open(SHARED_DATA_ENDPOINT).then(function(cache) {
return cache.match(SHARED_DATA_ENDPOINT).then(function (response) {
return response || new Response('{}');;
}) || new Response('{}');
})
);
}
} else {
return event;
}
});
We are listening for fetch event, and if request path matches our endpoint's name, we are either saving request body as a response to cache or (in case of GET request) we return saved response or empty one if there is nothing saved.
Step 2: Pushing data to cache
Once you have your service worker ready, now you can just create a post request with data you want to save in body for example:
fetch(SHARED_DATA_ENDPOINT, { method: "POST", body: JSON.stringify({ token: 'sampletoken' })}).then(() => {
console.log('saved to cache')
});
Before calling you should make sure that your service worker is active.
Step 3: Retrieving cached data
To fetch cached data you will have to call GET on your fake endpoint respectively. If there is no data available, you will get an empty object (see: new Response(‘{}’)) in our service worker. Also note that if you make that request before your service worker is registered, you will get an error, as that endpoint, in fact, does not exist.
fetch(SHARED_DATA_ENDPOINT).then(response => response.json()).then(data => {
console.log('Got', data, 'from cache');
});
Summary
And that’s it! Now you can share data between safari and PWA added to home screen. Just remember, that cache content is limited to 50MB and can be cleared after a certain period of time - but I guess for simply passing data between instances is just fine.
Stay tuned - article about how to make OAuth work on iOS PWA is coming soon!