2

I want to update PWA application when new content is available. Since we can't access DOM from service worker, how can I exactly do that? The service worker correctly recognize the new content, but I can't bind a hook or context to show my modal or update button in react. Any help?

service worker register and onUpdate function:

function RegisterValidSW(swUrl, 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.
          // setUpdate(true);
          // registration.waiting.postMessage({ type: 'SKIP_WAITING' });
          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.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);
});
}

this function is in the serviceWorkerRegistration.js file. We can add registration.waiting.postMessage({ type: 'SKIP_WAITING' }); function to call skipWaiting but it doesn't wait for user interaction and can't reload the page. how can I solve that?

2 Answers2

0

You can try passing in an onUpdate function when you register the service worker. In that function send a SKIP_WAITING message to the service worker followed a page reload.

 onUpdate: (registration) => {
    console.log("SW: sending SKIP_WAITING message to get updated serviceworker to take effect now (without manual restart)")
    // Old serviceworker will be stopped/new serviceworker started without reload
    registration.waiting.postMessage({type: 'SKIP_WAITING'})
    window.location.reload()
}

Your server worker should have SKIP_WAITING message hander. Something like this:

self.addEventListener('message', (event) => {
console.log("SW: received message event = "+JSON.stringify(event))
if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
}});

This is working for me.

What I am still struggling with is how to get the app updated if the user leaves the app open. As in,

  1. User browses to PWA url
  2. User adds to Home Screen
  3. User closes browser tab
  4. User opens PWA from Home Screen

and just leaves it open forever and ever...

This should probably go in a new question, but I thought it might be relevant to your use case.

ScottD
  • 64
  • 1
  • 11
0

basically, you have a PWA component for example. in this component, you create a dialog modal to show the user new updates. then you register the PWA inside of this component in componentDidMount or useEffect(() => {} , []). but in this way you pass an update function into register like this.

// ============= ServiceWorkerRegisteration.js

/* eslint-disable no-param-reassign */
// This optional code is used to register a service worker.
// register() is not called by default.

// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.

// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA

const isLocalhost = Boolean(
    window.location.hostname === 'localhost' ||
        // [::1] is the IPv6 localhost address.
        window.location.hostname === '[::1]' ||
        // 127.0.0.0/8 are considered localhost for IPv4.
        window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
);

// this part of code added for the times user close pwa or minimize it then comeback
// and we want to check for new updates
const registerPwaOpeningHandler = (registration, callback) => {
    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
            registration.update();
            if (callback) callback();
        }
    });
};

function registerValidSW(swUrl, config) {
    navigator.serviceWorker
        .register(swUrl)
        .then((registration) => {
            registerPwaOpeningHandler(registration, () => {
                const waitingWorker = registration.waiting;
                if (waitingWorker && waitingWorker.state === 'installed') {
                    if (config && config.onUpdate) {
                        config.onUpdate(registration);
                    }
                }
            });

            const waitingWorker = registration.waiting;
            if (waitingWorker && waitingWorker.state === 'installed') {
                if (config && config.onUpdate) {
                    config.onUpdate(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.
                            // eslint-disable-next-line no-console
                            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.
                            // eslint-disable-next-line no-console
                            console.log('Content is cached for offline use.');

                            // Execute callback
                            if (config && config.onSuccess) {
                                config.onSuccess(registration);
                            }
                        }
                    }
                };
            };
        })
        .catch((error) => {
            // eslint-disable-next-line no-console
            console.error('Error during service worker registration:', error);
        });
}

function checkValidServiceWorker(swUrl, config) {
    // Check if the service worker can be found. If it can't reload the page.
    fetch(swUrl, {
        headers: { 'Service-Worker': 'script' }
    })
        .then((response) => {
            // Ensure service worker exists, and that we really are getting a JS file.
            const contentType = response.headers.get('content-type');
            if (
                response.status === 404 ||
                (contentType != null && contentType.indexOf('javascript') === -1)
            ) {
                // No service worker found. Probably a different app. Reload the page.
                navigator.serviceWorker.ready.then((registration) => {
                    registration.unregister().then(() => {
                        window.location.reload();
                    });
                });
            } else {
                // Service worker found. Proceed as normal.
                registerValidSW(swUrl, config);
            }
        })
        .catch(() => {
            // eslint-disable-next-line no-console
            console.log('No internet connection found. App is running in offline mode.');
        });
}

export function register(config) {
    if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
        // The URL constructor is available in all browsers that support SW.
        const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
        if (publicUrl.origin !== window.location.origin) {
            // Our service worker won't work if PUBLIC_URL is on a different origin
            // from what our page is served on. This might happen if a CDN is used to
            // serve assets; see https://github.com/facebook/create-react-app/issues/2374
            return;
        }

        const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

        if (isLocalhost) {
            // This is running on localhost. Let's check if a service worker still exists or not.
            checkValidServiceWorker(swUrl, config);
            // Add some additional logging to localhost, pointing developers to the
            // service worker/PWA documentation.
            navigator.serviceWorker.ready.then(() => {
                // eslint-disable-next-line no-console
                console.log(
                    'This web app is being served cache-first by a service ' +
                        'worker. To learn more, visit https://cra.link/PWA'
                );
            });
        } else {
            // Is not localhost. Just register service worker
            registerValidSW(swUrl, config);
        }
    }
}

export function unregister() {
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.ready
            .then((registration) => {
                registration.unregister();
            })
            .catch((error) => {
                // eslint-disable-next-line no-console
                console.error(error.message);
            });
    }
}

// ============= PWA/index.js

import { useEffect, useState } from 'react';
import * as serviceWorker from '../serviceWorkerRegistration';
import UpdateVersionModal from './modal';

export default function Pwa() {
    const [registerWating, setRegisterWating] = useState(null);

    useEffect(() => {
        serviceWorker.register({
            onUpdate: (registration) => {
                if (registration && registration.waiting) {
                    setRegisterWating(registration);
                }
            }
        });
    }, []);

    const handleUpdate = () => {
        registerWating?.postMessage({ type: 'SKIP_WAITING' });
        setRegisterWating(null);
    };

    return (
        <UpdateVersionModal
            name="update-modal-version"
            open={Boolean(registerWating)}
            onClose={() => setRegisterWating(false)}
            onUpdate={handleUpdate}
        />
    );
}

// ============= PWA/dialog.js

/* eslint-disable react/prop-types */
import React from 'react';
import { Button } from '@mui/material';
import { ModalDialog, ModalContent, ModalAction } from './style';

class UpdateModal extends React.PureComponent {
    render() {
        const { open, onClose, onUpdate } = this.props;
        return (
            <ModalDialog open={open}>
                {/* <ModalTitle>asd</ModalTitle> */}
                <ModalContent>
                    <img alt="new update" src="/images/new-update.svg" className="update-image" />
                    <div className="content-text">
                        <p className="title">We’er Better Than Ever</p>
                        <p className="discription">
                            This version is no longer supported. Please update to the latest
                            version. Thanks
                        </p>
                    </div>
                </ModalContent>
                <ModalAction>
                    <Button color="primary" variant="contained" onClick={onClose}>
                        cancel
                    </Button>
                    <Button color="primary" variant="outlined" onClick={onUpdate}>
                        Update
                    </Button>
                </ModalAction>
            </ModalDialog>
        );
    }
}

export default UpdateModal;

registerPwaOpeningHandler function added to ServiceWorkerRegisteration for more compatibility like when user cancel the update modal and switch to another tab after he comeback to our tab again we show the dialog once more.

Amir Rezvani
  • 1,262
  • 11
  • 34