Implement the user update feature on React PWA

I want to implement a user-clicked app update feature of a React PWA. I want to show a Chakra-UI toast which has a button, on whose click the PWA updates.

This is my service worker file code so far

// import { env } from 'env';

import { clientsClaim, RouteMatchCallback } from 'workbox-core';
import { createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching';
import { NavigationRoute, registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';
import { Workbox } from 'workbox-window';

import { serverEndpoints } from '../api/endpoints';

import { platform } from './platformDetection';
import {
  APPLICATION_JSON,
  AUTHORIZATION,
  CONTENT_TYPE,
  DB,
  ErrCodes,
  deviceId,
  SwMessageType,
} from './swConstants';
import {
  factory,
  getBody,
  initToken,
  json,
  logger,
  routeMatcher,
  updateRequest,
} from './swUtils';

declare const self: ServiceWorkerGlobalScope;

// Connection to a broadcast channel
const bc = new BroadcastChannel('token_updates');

/** Buffer time between access token requests (4sec) */
const buffer = 4000 as const;

/**
 * Access token timeout duration.
 *
 * Value is set as the app configuration api response (`access_token_expiry_in_seconds`)
 *
 * @defaultValue 15min
 */
let accessTokenTimeout = 15 * 60 * 1000 - buffer;

/**
 * The id of setTimeout of access token.
 * This is used to detect if there's a current timeout set for access token
 */
let timerId: NodeJS.Timer | undefined;

let isInitCalled = false;
/**
 * Listens for the 'fetch' event and calls the `init()` function if it has not been called yet.
 * This ensures that the service worker is properly initialized before handling any fetch requests.
 */
self.addEventListener('fetch', () => {
  if (!isInitCalled) init();
});

const accessToken = initToken();
const refreshToken = initToken();
const subscription = initToken(); // initializing subscription token
clientsClaim();
self.skipWaiting();

precacheAndRoute(self.__WB_MANIFEST);

// initial setup for new worker
self.addEventListener('activate', init);

// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell.
// https://developer.chrome.com/docs/workbox/modules/workbox-routing/#how-to-register-a-navigation-route
registerRoute(
  new NavigationRoute(createHandlerBoundToURL('/index.html'), {
    denylist: [//docs*/],
  })
);

self.addEventListener('install', self.skipWaiting);

self.addEventListener('message', async (event) => {
  const data = event.data;
  if (!data) return;

  switch (data.type as SwMessageType) {
    case 'SKIP_WAITING':
      self.skipWaiting();
      break;
    case 'fetchaccesstoken':
      fetchAccessToken();
      break;
    default:
      try {
        await refreshToken;
        await accessToken;
      } catch (e) {
        logger.error('default postMessage', e);
      }
  }
});

/** Local Login */
registerRoute(
  routeMatcher('/login', location.origin),
  factory(async () =>
    json({
      is_logged_in: Boolean(await refreshToken.catch(() => false)),
    })
  )
);

/** Local logout */
registerRoute(
  routeMatcher('/logout', location.origin),
  factory(async () => {
    fetch(serverEndpoints.logout, {
      headers: [[AUTHORIZATION, `Bearer ${await accessToken.catch(() => '')}`]],
      method: 'PUT',
    });
    storeOrDeleteRefreshToken(true);
    accessToken.update(Promise.reject(ErrCodes.LOGIN_REQUIRED));
    refreshToken.update(Promise.reject(ErrCodes.LOGIN_REQUIRED));

    return json({ message: 'logged out' });
  })
);

// this route is specifically for ios but also can be used for android
registerRoute(
  routeMatcher('/subscribe', location.origin),
  factory(async (request) => {
    const req = await request.json();
    const existingSub = await self.registration.pushManager.getSubscription();
    try {
      if (existingSub) {
        subscription.update(Promise.resolve(existingSub.toString()));
        return json(existingSub);
      }
      const sub = await self.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: req.vapid,
      });
      console.log('ios sub : ', sub);
      subscription.update(Promise.resolve(sub.toString()));
      return json(sub);
    } catch (e: any) {
      return new Response(JSON.stringify({ message: e.message }), {
        status: 500,
        headers: { [CONTENT_TYPE]: APPLICATION_JSON },
      });
    }
  }),
  'POST'
);

// Local refresh route for refreshing the access token and send push notification token to server
// we are passing stringified Push Subscription or fcm token in the device_id field
registerRoute(
  routeMatcher('/refresh', location.origin),
  factory(async (request) => {
    let req = await request.json();
    subscription.update(Promise.resolve(req.device_id));
    let Body;
    if (platform == 'ios') {
      Body = JSON.stringify({ os: platform, device_id: req.device_id });
    } else {
      Body = JSON.stringify({ os: platform, device_id: req.device_id });
    }

    const res = await fetch(serverEndpoints.access_token, {
      method: 'POST',
      body: Body,
      headers: new Headers([
        [AUTHORIZATION, `Bearer ${await refreshToken}`],
        [CONTENT_TYPE, APPLICATION_JSON],
      ]),
    });
    return res;
  }),
  'POST'
);

//  Event listener for push notifications
self.addEventListener('push', async (event) => {
  console.log('1pushevent', event);
  const data = await event?.data?.json();
  console.log('noteventdata', data);
  let notificationTitle = data?.notification?.title || 'New Message Received';
  let notificationBody = data?.notification?.body || '';
  let notificationIcon = data?.notification?.image || '';
  let notificationData = data?.data || {};
  console.log('platform', platform);
  if (platform === 'ios') {
    notificationTitle = data?.title || notificationTitle;
    notificationBody = data?.body || notificationBody;
    notificationIcon = data?.image_url || notificationIcon;
    notificationData = {
      entity_type: data?.entity_type,
      entity_id: data?.entity_id,
    };
  }

  event.waitUntil(
    self.registration.showNotification(notificationTitle, {
      body: notificationBody,
      icon: notificationIcon,
      data: notificationData,
    })
  );
});

// self.addEventListener('push', function(event) {
//  console.log('Push message received.');
//  let notificationTitle = 'New Message Received';
//  const notificationOptions = {
//      body: 'Thanks for sending this push msg.',
//      icon: '/public/Logo.svg',
//      badge: '/public/Logo.svg',
//      data: {
//          url: 'https://web.dev/push-notifications-overview/',
//      },
//  };

//  if (event.data) {
//      const dataText = event.data.text();
//     console.log("event.data",dataText)
//      notificationTitle = 'Received Payload';
//      notificationOptions.body = `Push data: '${dataText}'`;
//  }

//  event.waitUntil(
//      self.registration.showNotification(
//          notificationTitle,
//          notificationOptions,
//      ),
//  );
// });

//  Event listener for notification click
/**
 * Event listener for notification click. When a notification is clicked, this function handles the navigation to the appropriate URL based on the notification data.
 *
 * @param {NotificationEvent} event - The notification click event.
 * @returns {Promise<void>} - A promise that resolves when the navigation is complete.
 */
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  console.log('clickevent', event);
  let url = `${location.origin}`;
  console.log('surl', url);
  let type = event.notification.data.entity_type;
  let id = event.notification.data.entity_id;

  switch (type) {
    case 'chat':
      url = url + '/chat-room/' + id;
      break;
    case 'trip':
      url = url + '/home?tripId=' + id;
      break;
    case 'request':
      url = url + '/home?tripId=' + id + '&viewRequest=true';
      break;
    default:
      // Handle the default case if needed
      break;
  }

  // Navigate to url
  event.waitUntil(
    self.clients
      .matchAll({ type: 'window', includeUncontrolled: true })
      .then((windowClients) => {
        for (let client of windowClients) {
          // if (client.url === url && 'focus' in client) {
          //   return client.focus();
          // }
          if (client.url.startsWith(location.origin)) {
            // Refresh the window with the new URL
            client.navigate(url);
            return client.focus();
          }
        }
        // If no existing window with the same URL is found, open a new one
        return self.clients.openWindow(url);
      })
  );
});

// OTP route
registerRoute(routeMatcher('/auth/otp'), new NetworkOnly(), 'POST');

// LOGIN route
registerRoute(
  routeMatcher('/auth/login'),
  factory(async (request, handler) => {
    const originalResponse = await handler.fetch(request);

    const data: any = await getBody(originalResponse);

    const a = data?.data?.access_token;
    const r = data?.data?.refresh_token;

    // save access and refresh tokens
    if (typeof a === 'string' && typeof r === 'string') {
      accessToken.update(Promise.resolve(a));
      refreshToken.update(Promise.resolve(r));

      storeOrDeleteRefreshToken();

      // strip tokens from response before sending to client
      return json({
        ...data,
        data: {},
      });
    } else return originalResponse;
  }),
  'POST'
);

/** SWR routes.
 * if `noCache` query param is passed, uses a network-only strategy and stores the result in cache,
 * otherwise uses SWR strategy
 */
// registerRoute(
//   ({ url }) =>
//     cache.user.keys.includes(url.pathname as UserCacheKeys) &&
//     url.origin === import.meta.env.VITE_API_BASE_URL,
//   factory(
//     async (request, handler) => {
//       try {
//         const url = new URL(request.url);
//         const noCacheKey = 'noCache' satisfies keyof User.CacheControlParam;
//         const noCache =
//           'true' ===
//           (url.searchParams.get(
//             noCacheKey
//           ) as User.CacheControlParam['noCache']);

//         url.searchParams.delete(noCacheKey);

//         const req = changeGetRequestURL(url, request, {
//           headers: addAuthHeader(request, await accessToken),
//         });

//         let res: Response;
//         if (noCache) {
//           res = await handler.fetchAndCachePut(req.clone());
//         } else {
//           res = await Promise.any([
//             handler.fetchAndCachePut(req.clone()),
//             handler.cacheMatch(req.clone()).then((res) => {
//               if (!res) throw res;
//               else return res;
//             }),
//           ]).catch((e) => {
//             if (e instanceof AggregateError) return Response.error();
//             else {
//               logger.error('SWR error', e);
//               throw e;
//             }
//           });
//         }

//         if (await isUnauthorized(res))
//           res = await handler.fetchAndCachePut(req);

//         if (url.pathname === cache.user.namedKeys.appConfiguration)
//           setAccessTokenTimeoutDuration(res);

//         return res;
//       } catch (e) {
//         if (e === ErrCodes.LOGIN_REQUIRED) {
//           return json({ message: 'Login required' }, 401);
//         }
//         logger.error('swr error', e);
//       }
//     },
//     { cacheName: cache.user.name }
//   )
// );

/** Matches corider server origin routes */
const defaultApiRouteMatcher: RouteMatchCallback = ({ url }) => {
  return url.origin === import.meta.env.VITE_API_BASE_URL;
};

/** Strategy for all corider apis, checks for 401 status code */
const defaultApiRouteHandler = factory(async (request, handler) => {
  try {
    const req = updateRequest(request, await accessToken);
    const res = await handler.fetch(req.clone());

    if (await isUnauthorized(res)) {
      const req = updateRequest(request, await accessToken);
      return await handler.fetch(req);
    } else {
      return res;
    }
  } catch (e) {
    if (e === ErrCodes.LOGIN_REQUIRED) {
      return json({ message: 'Login required' }, 401);
    }
    logger.error('defult route', e);
  }
});

registerRoute(defaultApiRouteMatcher, defaultApiRouteHandler, 'GET');
registerRoute(defaultApiRouteMatcher, defaultApiRouteHandler, 'POST');
registerRoute(defaultApiRouteMatcher, defaultApiRouteHandler, 'PUT');
registerRoute(defaultApiRouteMatcher, defaultApiRouteHandler, 'DELETE');
registerRoute(defaultApiRouteMatcher, defaultApiRouteHandler, 'PATCH');
registerRoute(defaultApiRouteMatcher, defaultApiRouteHandler, 'HEAD');

/**
 * Service Worker init function
 *
 * Creates IDB database and store if not already created,
 *
 * Fetches refresh token from DB if already existed, else throws.
 *
 * Fetches access token from network if refreh token is found.
 */
function init() {
  isInitCalled = true;
  logger.log('init called');
  const request = self.indexedDB.open(DB.name, DB.version);

  request.onblocked = (e) => {
    logger.error('init blocked');
    (e.target as IDBOpenDBRequest).result?.close();
  };

  request.onupgradeneeded = (e) => {
    const db = request.result;

    db.onerror = (e) => logger.error('Init db error ', e);

    if (db.objectStoreNames.contains(DB.store.name))
      db.deleteObjectStore(DB.store.name);

    db.createObjectStore(DB.store.name, DB.store.options).transaction.commit();
  };

  request.onsuccess = (e) => {
    const db = request.result;
    logger.log('IDB open req success');
    const transaction = db.transaction(DB.store.name, 'readonly');
    const refreshTokenQuery = transaction
      .objectStore(DB.store.name)
      .get(DB.refreshToken.key);

    refreshTokenQuery.onsuccess = async (e) => {
      if (typeof refreshTokenQuery.result === 'string') {
        logger.log('IDB ref success');
        refreshToken.update(Promise.resolve(refreshTokenQuery.result));
        fetchAccessToken();
      } else {
        refreshToken.update(Promise.reject(ErrCodes.LOGIN_REQUIRED));
        accessToken.update(Promise.reject(ErrCodes.LOGIN_REQUIRED));
      }
    };

    refreshTokenQuery.onerror = (e) => {
      logger.error('refresh token query errror', e);
      refreshToken.update(Promise.reject(ErrCodes.LOGIN_REQUIRED));
      accessToken.update(Promise.reject(ErrCodes.LOGIN_REQUIRED));
    };
  };

  request.onerror = (e) => {
    logger.error('init error', e);
  };
}

/**
 * Stores refresh token into IDB.
 *
 * Deletes it if `true` is passed as paramater
 */
function storeOrDeleteRefreshToken(deleteToken?: boolean) {
  const request = self.indexedDB.open(DB.name, DB.version);

  request.onsuccess = async (e: Event) => {
    const transaction = (e.target as IDBOpenDBRequest).result.transaction(
      DB.store.name,
      'readwrite'
    );

    if (deleteToken)
      transaction.objectStore(DB.store.name).delete(DB.refreshToken.key);
    else
      transaction
        .objectStore(DB.store.name)
        .put(await refreshToken, DB.refreshToken.key);
    transaction.commit();
  };

  request.onerror = (e) => {
    logger.error('IDB error', e);
  };
}

/** set a timeout for next access token fetch  */
function setAccessTokenTimer(timeout: number) {
  if (typeof timerId === 'undefined')
    timerId = setTimeout(fetchAccessToken, timeout);
}

/**
 * The only way access token should be used fetched.
 *
 * If the response has status `401`, rejects refresh token and access token, with {@link ErrCodes.LOGIN_REQUIRED} error code.
 *
 * If there was some other cause of failure (e.g. network failure), **resolves** the access token with garbage value
 */
async function fetchAccessToken() {
  logger.log('fetching access token...');

  clearTimeout(timerId);
  timerId = undefined;

  let resolver;
  const _a = new Promise<string>((res) => void (resolver = res));
  accessToken.update(_a, resolver);

  try {
    //using stored push subscription in each refresh request

    const res = await fetch(serverEndpoints.access_token, {
      method: 'POST',
      body: JSON.stringify({
        os: platform,
      }),
      headers: new Headers([
        [AUTHORIZATION, `Bearer ${await refreshToken}`],
        [CONTENT_TYPE, APPLICATION_JSON],
      ]),
    });
    if (res.status === 401) {
      // refresh token rejected
      refreshToken.update(Promise.reject(ErrCodes.LOGIN_REQUIRED));

      // Navigate to url
      let url = `${location.origin}/login`;

      self.clients
        .matchAll({ type: 'window', includeUncontrolled: true })
        .then((windowClients) => {
          for (let client of windowClients) {
            // if (client.url === url && 'focus' in client) {
            //   return client.focus();
            // }
            if (client.url.startsWith(location.origin)) {
              // Refresh the window with the new URL
              client.navigate(url);
              return client.focus();
            }
          }
          // If no existing window with the same URL is found, open a new one
          return self.clients.openWindow(url);
        });
      await refreshToken;
    }

    if (!res.ok) {
      throw res;
    }

    const data = await res.json().catch(() => undefined);

    if (typeof data?.access_token === 'string') {
      accessToken.update(Promise.resolve(data.access_token));

      bc.postMessage(data?.access_token);

      setAccessTokenTimer(accessTokenTimeout);
    } else
      throw new Error("invalid server response, couldn't refresh access token");
  } catch (e) {
    // Couldn't fetch access token
    logger.error('acc token', e);
    if (e === ErrCodes.LOGIN_REQUIRED)
      accessToken.update(Promise.reject(ErrCodes.LOGIN_REQUIRED));
    else accessToken.update(Promise.resolve(''));
  }

  return true;
}

/** Sets the timeout duration for access token from the user config api response */
async function setAccessTokenTimeoutDuration(res: Response) {
  const body: any = await getBody(res);
  const tokenTimeout = body?.data?.access_token_expiry_in_seconds;

  if (
    typeof tokenTimeout === 'number' &&
    !isNaN(tokenTimeout) &&
    tokenTimeout !== 0
  ) {
    accessTokenTimeout = tokenTimeout * 1000 - buffer;
  }
}

/** Checks for `401` status code, and fetches the access token if it's not pending.
 *
 * This check must be used in every new RouteHandler/fetch event listener
 */
async function isUnauthorized(res: Response) {
  if (res.status !== 401) return false;

  if (accessToken.state === 'fulfilled') {
    await fetchAccessToken();
  }

  return true;
}

I want to know a couple of things -:

  1. How to implement the toast coming feature on the React application. (I am using a Vite react app)
  2. I want to locally test out this feature. Any help in the testing of this app update feature would be extremely helpful.

I am a newbie with service workers and this is quite confusing for me.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật