Role of navigator.serviceWorker.controller in serviceWorker.js of Create React App - reactjs

Below is an excerpt of serviceWorker.js generated by CRA.
What is the point of having if (navigator.serviceWorker.controller) inside
if (installingWorker.state === 'installed')?
The new service worker has been successfully installed at this stage. Does the 'else' part handle the scenario in which no previous service worker has been installed yet?
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});

navigator.serviceWorker.controller will be set to a truthy value (a ServiceWorker object) if the current page is under the control of a service worker.
If this is the first time a service worker's been registered whose scope encompasses the current page's URL, then navigator.serviceWorker.controller will not be set (unless the service worker itself calls clients.claim()).
If this is a subsequent visit to a page that is under the registered service worker's scope, then navigator.serviceWorker.controller will be set, and the page will be under the service worker's control.

Related

Update user in database on success from Stripe prebuilt checkout

I am using Stripe's prebuilt checkout with react and firebase. The checkout process works fine and directs the user to the succes_url, but I would like to update a field under the user in the database as well. I don't understand how I can include a function that updates the DB that runs upon a successful checkout.
export const checkoutWithStripe = async(user) => {
const checkoutSessionsRef = collection(db, 'users', user.uid, 'checkout_sessions');
const singleCheckoutSessionRef = await addDoc(checkoutSessionsRef, {
price: 'price_xyz',
allow_promotion_codes: true,
success_url: `${window.location.origin}/dashboard/app?success=true`,
cancel_url: `${window.location.origin}/dashboard/app?canceled=true`
});
onSnapshot(singleCheckoutSessionRef, (snap) => {
const { error, url: checkoutUrl } = snap.data();
if (error) {
console.error(`An checkout error occured: ${error.message}`);
}
if (checkoutUrl) {
window.location.assign(checkoutUrl);
}
});
// TODO: Update user type in firebase from free to starter on successful checkout
};
Thankful for any help.
Update:
I solved it, in 2 parts.
In Stripe I created a new webhook that points to a exported firebase function (2) that fires when "checkout.session.completed" is fired.
In Firebase i created a function that listens for the "checkout.session.completed" event and then calls a function that updates the DB based on the user email that I get from the Stripe event.
This is the Firebase function that listens to the event:
/**
* A webhook handler function for the relevant Stripe events.
*/
exports.updateCustomer = functions.https.onRequest((req, resp) => {
functions.logger.log("updateCustomer body", req);
const relevantEvents = new Set([
'checkout.session.completed'
]);
let event;
// Instead of getting the `Stripe.Event`
// object directly from `req.body`,
// use the Stripe webhooks API to make sure
// this webhook call came from a trusted source
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
req.headers['stripe-signature'],
endpointSecret
);
} catch (error) {
functions.logger.log(`Webhook Error: Invalid Secret`);
resp.status(401).send('Webhook Error: Invalid Secret');
return;
}
functions.logger.log("updateCustomer", event.type);
if (relevantEvents.has(event.type)) {
// logs.startWebhookEventProcessing(event.id, event.type);
try {
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
functions.logger.log("checkout.session.completed:", session);
updatePlan(session.customer_details.email);
break;
default:
functions.logger.log(`Unhandled event type ${event.type}`);
}
} catch (error) {
functions.logger.log(`Unhandled event error ${event.type}`);
resp.json({
error: 'Webhook handler failed. View function logs in Firebase.',
});
return;
}
}
// Return a response to Stripe to acknowledge receipt of the event.
resp.json({ received: true });
});
If you need to run some code when the Checkout Session is successful, then you should use Stripe webhooks and listen to the checkout.session.completed event. This is covered in the Stripe documentation.

Create React App PWA - change caching strategy of service worker

I have created a PWA template using CRA v4 and enabled the service worker that comes with it by registering it, because I needed to create a pop up notification about installing the PWA.
The lighthouse test has to pass for the app to be PWA compatible so that the browser would fire the beforeinstallprompt event listener needed to detect if the user has already installed the PWA or not.
The problem now is that this service worker is using cache-first strategy. As a result refreshing the page does not trigger an update and I am left with an older version of the app appearing after I have deployed an update.
How can I change the caching strategy of CRA v4's service worker such that the user would get a new version of the app by simply refreshing the page?
I am also interested in knowing why this cache-first strategy is used by default. To me it seems bad that the user has to close every tab to get a new version. Why haven't more people brought this up? This is clearly not user friendly...
To change the strategy you need to implement your own code changing service-worker.js and potentially
serviceWorkerRegistration.js. (https://developers.google.com/web/tools/workbox/modules/workbox-strategies)
I implement my own strategy:
check for updates at the very beginning check for updates each 3 min if there is a update in the very beginning update
cache and refresh the website. if it is after show a popup asking to fresh
serviceWorkerRegistration.ts
/**
* ...... previous code
**/
const CHECK_INTERVAL_TIME = 1000 * 60 * 3 // 3 min
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://cra.link/PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.info('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
/****************************
start new code
*****************************/
registration.update().then(() => console.debug("Checked for update...")).catch(console.error)
setInterval(() => {
console.debug("Checked for update...");
registration.update().catch(console.error);
}, CHECK_INTERVAL_TIME);
/****************************
end new code
*****************************/
})
.catch((error) => {
console.error('Error during service worker registration:', error);
});
}
App.tsx
/**
* ...... previous code
**/
function App() {
const time = useRef(Date.now()); //can be let, depending of your logic
useEffect(() => {
serviceWorkerRegistration.register({
onSuccess(registration) {
console.debug('serviceWorkerRegistration success')
},
onUpdate(registration) {
console.debug('serviceWorkerRegistration updated',Date.now()-time.current)
const refresh=async ()=>{
await registration?.waiting.postMessage({type: 'SKIP_WAITING'}); //send message to update the code (stop waiting)
if ('caches' in window) { //delete cache, i think is no necessary but you lose nothing
const names = await caches.keys()
for (const name of names) {
await caches.delete(name)
}
}
window.location.reload();
}
if (Date.now()-time.current<=2000){
return refresh()
}
logicToShowPopup({
onClick: refresh
})
}
})
}, [])
return (<div>My App</div>)
}
I hope this suits for your needs

Google Analytics ga('set') equivalant on Google Tag Manager

Quoted from https://developers.google.com/analytics/devguides/collection/analyticsjs/single-page-applications
When your application loads content dynamically and updates the URL in
the address bar, the data stored on your tracker should be updated as
well.
ga('set', 'page', '/new-page.html');
How can I run this if I am using Google Tag Manager with Google Analytics?
Below is snippet of how I track for pageview whenever route change on React router using higher order component
componentDidMount() {
const { history } = this.props;
this.history = history.listen(location => {
if (history.action === 'PUSH') {
// location change
if (window && window.dataLayer) {
const dataLayer = window.dataLayer || [];
const { pathname, search } = location;
// how to achieve this with Google Tag Manager?
// ga.set('page', `${pathname}${search}`);
setTimeout(() => {
dataLayer.push({
event: 'Pageview',
});
}, 1000);
}
}
});
}
If you're using GTM, then I can recommend two options.
Option 1, if your app actually updates the URL as routing changes:
Create a custom event trigger to track that Pageview event you've pushed to the data layer.
Go into "Variables" and make sure you have "Page Path" selected.
Create a new GA Tag and make sure you check "Enable overriding settings in this tag" and then on the bottom there is drop down for "More Settings" and you'll see "Fields to Set", click on "Add Field" and you can add "page" and set the value to the {{Page Path}}. Then add in the trigger created in step 1
Save and run and you should be sending pageview data to GA via this event. The path will be whatever that's in the browser's url bar.
If you want an even more customized path, then you can modify your code to the following, we added "page-path" variable to the data layer.
componentDidMount() {
const { history } = this.props;
this.history = history.listen(location => {
if (history.action === 'PUSH') {
// location change
if (window && window.dataLayer) {
const dataLayer = window.dataLayer || [];
const { pathname, search } = location;
// how to achieve this with Google Tag Manager?
// ga.set('page', `${pathname}${search}`);
setTimeout(() => {
dataLayer.push({
page-path: ${pathname}${search},
event: 'Pageview'
});
}, 1000);
}
}
});
}
Then you need to setup a new datalayer variable to capture this in GTM:
Then go back to the GA tag created before and modify the "page" field value to this new variable:

How to activate a react route and pass data from the service worker?

I have a SPA PWA React app.
It is installed and running in standalone mode on the mobile device (Android+Chrome).
Let's say the app lists people and then when you click on a person it diplays details using /person route.
Now, I'm sending push notifications from the server and receiving them in the service worker attached to the app. The notification is about a person and I want to open that person's details when the user clicks on the notification.
The question is:
how do I activate the /person route on my app from the service worker
and pass data (e.g. person id, or person object)
without reloading the app
From what I understand, from the service worker notificationclick event handler I can:
focus on the app (but how do I pass data and activate a route)
open an url (but /person is not a physical route, and either way - I want avoid refreshing the page)
You can listen for click event for the Notification which you show to the user. And in the handler, you can open the URL for the corresponding person which comes from your server with push event.
notification.onclick = function(event) {
event.preventDefault();
// suppose you have an url property in the data
if (event.notification.data.url) {
self.clients.openWindow(event.notification.data.url);
}
}
Check these links:
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/notificationclick_event
https://developer.mozilla.org/en-US/docs/Web/API/Clients/openWindow
To answer my own question: I've used IndexedDB (can't use localStorage as it is synchronous) to communicate between SW and PWA, though I'm not too happy about it.
This is roughly how my service worker code looks (I'm using idb library):
self.addEventListener('notificationclick', function(event) {
const notif = event.notification;
notif.close();
if (notif.data) {
let db;
let p = idb.openDB('my-store', 1, {
upgrade(db) {
db.createObjectStore(OBJSTORENAME, {
keyPath: 'id'
});
}
}).then(function(idb) {
db = idb;
return db.clear(OBJSTORENAME);
}).then(function(rv) {
return db.put(OBJSTORENAME, notif.data);
}).then(function(res) {
clients.openWindow('/');
}).catch(function(err) {
console.log("Error spawning notif", err);
});
event.waitUntil(p);
}
});
and then, in the root of my react app ie in my AppNavBar component I always check if there is something to show:
componentWillMount() {
let self = this;
let db;
idb.openDB('my-store', 1)
.then(function (idb) {
db = idb;
return db.getAll(OBJSTORENAME);
}).then(function (items) {
if (items && items.length) {
axios.get(`/some-additional-info-optional/${items[0].id}`).then(res => {
if (res.data && res.data.success) {
self.props.history.push({
pathname: '/details',
state: {
selectedObject: res.data.data[0]
}
});
}
});
db.clear(OBJSTORENAME)
.then()
.catch(err => {
console.log("error clearing ", OBJSTORENAME);
});
}
}).catch(function (err) {
console.log("Error", err);
});
}
Have been toying with clients.openWindow('/?id=123'); and clients.openWindow('/#123'); but that was behaving strangely, sometimes the app would stall, so I reverted to the IndexedDB approach.
(clients.postMessage could also be the way to go though I'm not sure how to plug that into the react framework)
HTH someone else, and I'm still looking for a better solution.
I had a similar need in my project. Using your's postMessage tip, I was able to get an event on my component every time a user clicks on service worker notification, and then route the user to the desired path.
service-worker.js
self.addEventListener("notificationclick", async event => {
const notification = event.notification;
notification.close();
event.waitUntil(
self.clients.matchAll({ type: "window" }).then(clientsArr => {
if (clientsArr[0]) {
clientsArr[0].focus();
clientsArr[0].postMessage({
type: "NOTIFICATION_CLICK",
ticketId: notification.tag,
});
}
})
);
});
On your react component, add a new listener:
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", message => {
if (message.data.type === "NOTIFICATION_CLICK") {
history.push(`/tickets/${message.data.ticketId}`);
}
});
}
}, [history]);

React & Service Worker - Cached Content - Inform user

I have a question regarding service workers and reactjs.
The recommendation is to inform the user about cached content or when new content is available, so that the user knows about cached content.
My question is now, how can I inform the user?
When I use create-react-app, in the registerServiceWorker.js there is this code, where it says:
At this point, the old content will have been purged and the
fresh content will have been added to the cache. It's the perfect
time to display a "New content is available; please refresh."
message in your web app.
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
But actually on this script, of course, I do not have access to the document, because the service worker works away from the script.
How can I handle this in an react component?
How do others handle this issue and inform the user?
The code included in your question runs inside the context of the window client. You have full control over showing whatever UI elements you'd like. Feel free to modify those console.log() statements and display something instead.
(There's separate code that runs in the context of the service worker, and you're correct that that code doesn't have access to the DOM. But that's not the code you're asking about.)

Resources