Using a service worker to redirect - javascript

I have a use case with my service worker where on certain occasions I need to either reload the page, or redirect the page.
We currently use the service worker as an interceptor for all navigation fetches. Because of this it's important to reload the page when a service worker is activated. I currently do this in the activated event as such:
function getClientList(): Promise<readonly WindowClient[]> {
return self.clients.claim().then(() =>
self.clients.matchAll({
type: 'window'
})
);
}
self.addEventListener('activate', async (event: ExtendableEvent) => {
console.log('Service Worker activated');
// we need to reload the page here so that the navigation fetch request is caught
const clientList = await getClientList();
for (const client of clientList) {
const url = new URL(client.url);
await client.navigate(`${url.pathname}${url.search}`);
}
});
This seems to be working without any issues, I am just concerned as to whether this is the best approach.
The second condition is that I need to redirect the user when the url in a service worker's navigation fetch event has a certain query param in it.
async function handleNavigationRequest(request: Request, workerLocation: WorkerLocation): Promise<Response> {
const requestURL = new Url(request.url)
const lid = requestURL.searchParams.get('lid');
const code = requestURL.searchParams.get('code');
const optin = requestURL.searchParams.get('optin');
const dpp = !!rbdsCode && !!loyaltyId && !!optin;
if (dppRequest) {
const clientList = await getClientList();
for (const client of clientList) {
const url = new URL(`https://some-domain.com/#/getTokens?brand=${BRAND}&code=${code}&lid=${loyaltyId}&optin=${optin}&env=${DPP_SSO_ENV}`);
await client.navigate(url);
}
}
}
self.addEventListener('fetch', (event) => {
if (event.request.method === 'GET' && event.request.mode === 'navigate') {
event.respondWith(
handleNavigationRequest(event.request, self.location)
);
}
});
This second part is partially working. It seems the first time the service worker is activated, it reloads the page as expected in step 1, and then picks up the correct query params in step 2 and performs the redirect.
However if I simply run a request with the same query params on an instance where the service worker has already been activated, I get an error in my console stating...
Uncaught (in promise) TypeError: Cannot navigate to URL: https://some-domain.com/#/getTokens?brand=someBrand&code=572896&lid=7Rewards&optin=Y&env=dev

Related

Prompting only once when there are multiple workbox-broadcast-update messages

I'm using Workbox and the BroadcastUpdatePlugin() in my serviceWorker to prompt the user to refresh the page when a cached file is updated. It works great when there's only one file updated, but when I publish multiple updates at once (HTML, CSS and JS files), the user is prompted to refresh the page for each file.
How can I update all files in the cache, then prompt the user to refresh the page only once, when the event listener has stopped receiving update messages?
ServiceWorker code
const {BroadcastUpdatePlugin} = workbox.broadcastUpdate;
registerRoute(
({request}) => request.destination === 'document',
new StaleWhileRevalidate({
//new NetworkOnly({
cacheName: 'pages',
plugins: [
new BroadcastUpdatePlugin(),
],
})
)
registerRoute(
({request}) => request.destination === 'script' || request.destination === 'style',
new StaleWhileRevalidate({
//new NetworkOnly({
cacheName: 'assets',
plugins: [
new BroadcastUpdatePlugin(),
],
})
)
JavaScript code
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js', { scope: '/' }).then(swReg => {
console.log('Service Worker Registered', swReg);
}).catch(error => {
console.log('There was an error!', error);
})
})
// Listen for cache updates and prompt a page reload
navigator.serviceWorker.addEventListener('message', async (event) => {
if (event.data.meta === 'workbox-broadcast-update') {
const {cacheName, updatedURL} = event.data.payload;
const cache = await caches.open(cacheName);
const updatedResponse = await cache.match(updatedURL);
const updatedText = await updatedResponse.text();
console.log('Updated: '+cacheName+', '+updatedURL);
// prompts for every update
if(confirm('Content Updated. Please refresh the page.')){
window.location.reload;
}
}
})
}
I could imagine there potentially being logic in your service worker that could help with this, by, for instance, looking at the cliendId in the FetchEvent that triggered an update and only conditionally sending the message if it hasn't seen that clientId before.
But that would require a bunch of custom logic beyond what BroadcastUpdatePlugin provides already, and it might be error prone—if your service worker broadcasts an update, then the user acts on the update, and then there's another update that could be broadcast, you probably want to give the user another chance to act on it, but it would be difficult for the service worker to know whether to broadcast a message to the same clientId in that scenario.
A cleaner approach would be to move the logic that prevented multiple prompt to the window client code.
There are a couple of ways to go there, but what seems the cleanest to me would rely on the resolution of a promise to trigger the prompt. You can call the resolve function of a promise multiple times, but the then() in the promise chain will only be invoked once, giving you the behavior you're looking for.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
const waitForUpdate = new Promise((resolve) => {
navigator.serviceWorker.addEventListener('message', async (event) => {
if (event.data.meta === 'workbox-broadcast-update') {
const {cacheName, updatedURL} = event.data.payload;
const cache = await caches.open(cacheName);
const updatedResponse = await cache.match(updatedURL);
const updatedText = await updatedResponse.text();
console.log(`Updated ${updatedURL} in ${cacheName}: ${updatedText}`);
// Calling resolve() will trigger the promise's then() once.
resolve();
}
});
});
waitForUpdate.then(() => {
if (confirm('Content updated. Please refresh the page.')) {
window.location.reload();
}
});
}
(You could also use an approach that relied on a global variable or something similar that gets flipped the first time you show the prompt, and short-circuits the prompt display every subsequent time, but I like using promises for this sort of thing.)

How to configure React Native Linking?

I am using deep linking for handling notifications in my app. For remote notifications I use React Native Firebase and for local notifications I use react-native-push-notification. I am handling background and quit state notifications with react-navigation's deep linking system, but for foreground notifications I use react-native's Linking and this is where the problem is. Linking.canOpenUrl returns supported but Linking.openUrl does nothing. I already added LSApplicationQueriesScheme key to my info.plist and added my prefix to it.
this is for foreground handling =>
const onOpenNotification = async (data) => {
const messagesObj = await AsyncStorage.getItem('messages')
const messages = JSON.parse(messagesObj)
const notificationData = messages[data.id]
Linking.canOpenURL(notificationData.link).then(supported => {
if (supported) {
console.log("supported!")
Linking.openURL(notificationData.link)
}
else {
console.log("Cannot open! ")
}
}).catch(err => console.log(err))
}
this is for background and quit state handling =>
export const notificationLinking = {
prefixes: ['rateet://app'],
config,
async getInitialUrl(){
// Check if app was opened from a deep link
const url = await Linking.getInitialURL();
if (url != null) {
return url;
}
// Check if there is an initial firebase notification
const message = await messaging().getInitialNotification();
// Get the `url` property from the notification which corresponds to a screen
// This property needs to be set on the notification payload when sending it
return message?.data.link;
},
subscribe(listener){
const onReceiveURL = (url) => listener(url);
// Listen to incoming links from deep linking
Linking.addEventListener('url', onReceiveURL);
// Listen to firebase push notifications
const unsubscribeNotification = messaging().onNotificationOpenedApp(
(message) => {
const url = message.data.link;
if (url) {
// Any custom logic to check whether the URL needs to be handled
//...
// Call the listener to let React Navigation handle the URL
listener(url);
}
}
);
return () => {
// Clean up the event listeners
Linking.removeEventListener('url', onReceiveURL);
unsubscribeNotification();
}
}
}

Trying to load dynamic content on a specific site without affecting other sites

I've followed a guide on how to add dynamic content to the cache and and show a error message through a fallback.json file (the error message is supposed to show up in same div where the dynamic content is).
The problem I am facing right now is that the website got multiple pages and I do only want to show the fallback text if I haven't loaded dynamic content on a specific site (the index site on this example). I am currently getting the fallback text on sites I haven't visited when I go offline.
So my question is:
How can the service worker act differently on different sites?
And can I for example use a JS method instead of using a fallback.json file?
Serviceworker.js:
const staticAssets = [
'./',
'./favicon.ico',
'./fallback.json'
];
// Call Install Event
self.addEventListener('install', async e => {
console.log("From SW: Install Event");
const cache = await caches.open('staticCache');
cache.addAll(staticAssets);
});
// Call Fetch Event
self.addEventListener('fetch', async event => {
console.log('Service Worker: Fetching');
const req = event.request;
const url = new URL(req.url);
event.respondWith(networkFirst(req));
});
// Network first for latest articles - if not available get cached
async function networkFirst(req) {
const cache = await caches.open('dynamicContent');
try {
// Try go through the network
const res = await fetch(req,);
// Store to cache
cache.put(req, res.clone());
return res;
} catch (e) {
// IF offline - return from cache
const cachedResponse = await cache.match(req);
// Return cachedresponce first, if no cache found - return fallback.json
return cachedResponse || await caches.match('./fallback.json');
}
}
And my manifest.json file:
{
"articles": [
{
"title": "No articles found!!",
"url": "",
"urlToImage": "",
"description": "Try reload your page once again when you are online."
}
]
}
I managed to solve this by using a IndexedDB. My mistake was trying to save dynamic content in the cache. The better solution for me was to handle and load data in a IndexedDB.
Reference: https://developers.google.com/web/ilt/pwa/working-with-indexeddb

How do I auto-refresh a JWT in redux without breaking async flow?

High-level description
I have a React/redux/electron app that uses Google Oauth. I want to be able to refresh the access token automatically when it expires. I've researched this and solved it semi-successfully using middleware, but my solution is erroring in certain situations.
I've implemented a refresh middleware that runs on every API action. It checks whether the access token is expired or about to expire. If so, instead of dispatching the action it received, it dispatches a token refresh action and queues up any other actions until a new access token is received. After that, it dispatches all actions in its queue.
However, one of my action creators looks something like this:
function queryThreads(params) {
return async (dispatch) => {
const threads = await dispatch(fetchThreads(params))
const newPageToken = threads.payload.nextPageToken
}
}
When the refresh middleware doesn't run because the token isn't expiring, threads.payload will be defined here and everything will work as intended.
However, when the refresh middleware does run, threads.payload will be undefined because the dispatch seems to resolve with the value of the token refresh action, rather than the fetchThreads action.
How do I ensure that the token gets refreshed (and updated in state/localStorage), fetchThreads gets dispatched with the updated token, and the threads variable gets assigned to the resolved value of the correct Promise?
Links to Project Code
This is my refresh middleware. It was inspired by this article by kmmbvnr.
This is the token refresh action creator.
This is the line in my queryThreads action creator that throws when the token has to refresh (threads.payload is undefined).
This is the reducer where I update state in response to a token refresh.
This is the middleware where I update localStorage in response to a token refresh.
It looks like I've solved the issue by rewriting the refresh middleware like this:
function createRefreshMiddleware() {
const postponedRSAAs = [];
return ({ dispatch, getState }) => {
const rsaaMiddleware = apiMiddleware({ dispatch, getState });
return next => action => {
if (isRSAA(action)) {
try {
const auth = JSON.parse(localStorage.getItem('auth'));
const { refresh_token: refreshToken } = auth;
const expirationTime = jwtDecode(auth.id_token).exp * 1000;
const isAccessTokenExpiring =
moment(expirationTime) - moment() < 300000;
if (refreshToken && isAccessTokenExpiring) {
postponedRSAAs.push(action);
if (postponedRSAAs.length === 1) {
return rsaaMiddleware(next)(
dispatch(() => attemptTokenRefresh(refreshToken))
).then(() => {
const postponedRSAA = postponedRSAAs.pop();
return dispatch(postponedRSAA);
});
}
return rsaaMiddleware(next)(action);
}
return rsaaMiddleware(next)(action);
} catch (e) {
console.log(e);
return next(action);
}
}
return next(action);
};
};
}
export default createRefreshMiddleware();
Now the postponed action will always be chained off of the token refresh action, so we don't have the problem of the original promise resolving with the wrong value; plus it's more concise.

ReactJS/nextJS: Don't get access to cookie/properties after redirecting

In my nextJS application the user can do a login (login page), which creates a cookie with a token and redirects to a route (main page), which uses the main component.
In the Main component I'm using getInitialProps to get the cookie token. But this is only working after refreshing the page.
So right now I'm logging in and getting redirected. If I click on the button, there is no token. After refreshing the page, I do get a token.
How can I avoid this? I think I'm doing something wrong on server side...
Login
export class Login extends Component {
render () {
return (
<Form id='login' onSubmit={this._onSubmit.bind(this)}>
// Input fields
</Form>
)
}
_onSubmit = (event) => {
event.preventDefault()
const { username, password } = this.state
// Sends mutation request to graphQL server
this.props.signinUserMutation({
variables: { username, password }
}).then(response => {
// Returns token
const token = response.data.token
if (token) {
Cookies.set('auth-token', token)
this.props.client.resetStore().then(() => {
redirect({}, '/main')
})
}
})
}
}
Main
class Main extends Component {
static async getInitialProps (context, apolloClient) {
const { req } = context
const initProps = {}
if (req && req.headers) {
const cookies = req.headers.cookie
if (typeof cookies === 'string') {
const cookiesJSON = jsHttpCookie.parse(cookies)
initProps.token = cookiesJSON['auth-token']
}
}
return initProps
}
clickIt = () => {
console.log(this.props.token)
}
render () {
return (<Button onClick={this.clickIt.bind(this)})
}
}
After login your cookie is saved in browser, so you should use document.cookie in getInitialProps to get cookie value.
As I understand this code:
if (req && req.headers) {
const cookies = req.headers.cookie
if (typeof cookies === 'string') {
const cookiesJSON = jsHttpCookie.parse(cookies)
initProps.token = cookiesJSON['auth-token']
}
}
is using request header to get cookie value, which will happen only after you make request to the server (when you refresh page). When navigation in browser happens that code doesn't run and you don’t get cookie. Thats why you need to get your cookie directly from browser document.cookie.
Consider using localStorage for saving auth tokens instead cookie, it has better api.
Since componentDidMount is executed only in the browser, use it to take cookie value and place it in redux store:
componentDidMount() {
placeTokenInReduxStoreAction(Cookies.get('auth-token'));
}
I assume you use cookies-js

Categories

Resources