From 1469d2146e0324dfee7c20711866266c3c921f20 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Wed, 9 Mar 2022 13:55:54 -0600 Subject: [PATCH] app: rework service worker registration - registration is moved from top-level index to app/sagas. - mock implementation is provided for tests - actions are renamed to include "app" prefix - actions are changed to not use non-serializable arguments - reducers are changed to not use non-serializable state --- src/__mocks__/serviceWorkerRegistration.ts | 52 +++++++++++ src/app/actions.ts | 18 ++-- src/app/reducers.test.ts | 63 ++++++------- src/app/reducers.ts | 18 ++-- src/app/sagas.test.ts | 103 ++++++++++++++++----- src/app/sagas.ts | 80 ++++++++++++---- src/index.tsx | 13 --- src/notifications/sagas.test.ts | 10 +- src/notifications/sagas.ts | 14 ++- src/reportWebVitals.ts | 2 + src/service-worker.ts | 2 + src/service-worker/actions.ts | 18 ++-- src/serviceWorkerRegistration.ts | 2 + src/settings/SettingsDrawer.tsx | 16 ++-- 14 files changed, 269 insertions(+), 142 deletions(-) create mode 100644 src/__mocks__/serviceWorkerRegistration.ts diff --git a/src/__mocks__/serviceWorkerRegistration.ts b/src/__mocks__/serviceWorkerRegistration.ts new file mode 100644 index 00000000..923688e2 --- /dev/null +++ b/src/__mocks__/serviceWorkerRegistration.ts @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +// mock implementation of serviceWorkerRegistration for testing + +type Config = { + onSuccess?: (r: ServiceWorkerRegistration) => void; + onUpdate?: (r: ServiceWorkerRegistration) => void; +}; + +/** The config that was registered, if any. */ +let globalConfig: Config | undefined; + +/** + * Mocks the register() function. + * + * This just saves the config for later use. + * + * @param config The config. + */ +export function register(config?: Config): void { + globalConfig = config; +} + +/** + * Fires the onSuccess callback that was registered, if any. + * + * @param registration The registration to pass to the callback. + */ +export function _fireOnSuccess(registration: ServiceWorkerRegistration): void { + if (globalConfig && globalConfig.onSuccess) { + globalConfig.onSuccess(registration); + } +} + +/** + * Fires the onUpdate callback that was registered, if any. + * + * @param registration The registration to pass to the callback. + */ +export function _fireOnUpdate(registration: ServiceWorkerRegistration): void { + if (globalConfig && globalConfig.onUpdate) { + globalConfig.onUpdate(registration); + } +} + +/** + * Mocks the unregister() function. + */ +export function unregister(): void { + // nothing to do for now +} diff --git a/src/app/actions.ts b/src/app/actions.ts index db3fd750..2be6281d 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -5,22 +5,18 @@ import { createAction } from '../actions'; -/** Creates an action that requests the app to reload. */ -export const reload = createAction((registration: ServiceWorkerRegistration) => ({ +/** Action that requests the app to reload. */ +export const appReload = createAction(() => ({ type: 'app.action.reload', - registration, })); -/** Action that requests to check for updates. */ -export const checkForUpdate = createAction( - (registration: ServiceWorkerRegistration) => ({ - type: 'app.action.checkForUpdate', - registration, - }), -); +/* Action that requests to check for updates. */ +export const appCheckForUpdate = createAction(() => ({ + type: 'app.action.checkForUpdate', +})); /** Action that indicates that checking for an update has completed. */ -export const didCheckForUpdate = createAction((updateFound: boolean) => ({ +export const appDidCheckForUpdate = createAction((updateFound: boolean) => ({ type: 'app.action.didCheckForUpdate', updateFound, })); diff --git a/src/app/reducers.test.ts b/src/app/reducers.test.ts index 21657d1b..603a333a 100644 --- a/src/app/reducers.test.ts +++ b/src/app/reducers.test.ts @@ -7,11 +7,11 @@ import { serviceWorkerDidUpdate, } from '../service-worker/actions'; import { + appCheckForUpdate, + appDidCheckForUpdate, appDidReceiveBeforeInstallPrompt, appDidResolveInstallPrompt, appShowInstallPrompt, - checkForUpdate, - didCheckForUpdate, didInstall, } from './actions'; import reducers from './reducers'; @@ -23,57 +23,52 @@ test('initial state', () => { Object { "checkingForUpdate": false, "hasUnresolvedInstallPrompt": false, + "isServiceWorkerRegistered": false, "promptingInstall": false, "readyForOfflineUse": false, - "serviceWorker": null, "updateAvailable": false, } `); }); -test('serviceWorker', () => { - const registration = {} as ServiceWorkerRegistration; - expect( - reducers( - { serviceWorker: null } as State, - serviceWorkerDidSucceed(registration), - ).serviceWorker, - ).toBe(registration); +describe('isServiceWorkerRegistered', () => { + it('should be true when service worker registration succeeds', () => { + expect( + reducers( + { isServiceWorkerRegistered: false } as State, + serviceWorkerDidSucceed(), + ).isServiceWorkerRegistered, + ).toBe(true); + }); }); test('checkingForUpdate', () => { expect( - reducers( - { checkingForUpdate: false } as State, - checkForUpdate({} as ServiceWorkerRegistration), - ).checkingForUpdate, - ).toBe(true); - expect( - reducers({ checkingForUpdate: false } as State, didCheckForUpdate(true)) - .checkingForUpdate, - ).toBe(false); - expect( - reducers({ checkingForUpdate: true } as State, didCheckForUpdate(true)) + reducers({ checkingForUpdate: false } as State, appCheckForUpdate()) .checkingForUpdate, ).toBe(true); expect( - reducers({ checkingForUpdate: true } as State, didCheckForUpdate(false)) + reducers({ checkingForUpdate: false } as State, appDidCheckForUpdate(true)) .checkingForUpdate, ).toBe(false); expect( - reducers( - { checkingForUpdate: true } as State, - serviceWorkerDidUpdate({} as ServiceWorkerRegistration), - ).checkingForUpdate, + reducers({ checkingForUpdate: true } as State, appDidCheckForUpdate(true)) + .checkingForUpdate, + ).toBe(true); + expect( + reducers({ checkingForUpdate: true } as State, appDidCheckForUpdate(false)) + .checkingForUpdate, + ).toBe(false); + expect( + reducers({ checkingForUpdate: true } as State, serviceWorkerDidUpdate()) + .checkingForUpdate, ).toBe(false); }); test('updateAvailable', () => { expect( - reducers( - { updateAvailable: false } as State, - serviceWorkerDidUpdate({} as ServiceWorkerRegistration), - ).updateAvailable, + reducers({ updateAvailable: false } as State, serviceWorkerDidUpdate()) + .updateAvailable, ).toBe(true); }); @@ -124,9 +119,7 @@ describe('promptingInstall', () => { test('readyForOfflineUse', () => { expect( - reducers( - { readyForOfflineUse: false } as State, - serviceWorkerDidSucceed({} as ServiceWorkerRegistration), - ).readyForOfflineUse, + reducers({ readyForOfflineUse: false } as State, serviceWorkerDidSucceed()) + .readyForOfflineUse, ).toBe(true); }); diff --git a/src/app/reducers.ts b/src/app/reducers.ts index 870d5e85..7b450fb4 100644 --- a/src/app/reducers.ts +++ b/src/app/reducers.ts @@ -9,31 +9,29 @@ import { serviceWorkerDidUpdate, } from '../service-worker/actions'; import { + appCheckForUpdate, + appDidCheckForUpdate, appDidReceiveBeforeInstallPrompt, appDidResolveInstallPrompt, appShowInstallPrompt, - checkForUpdate, - didCheckForUpdate, didInstall, } from './actions'; -const serviceWorker: Reducer = ( - state = null, - action, -) => { +/** Indicates that the service worker was successfully registered. */ +const isServiceWorkerRegistered: Reducer = (state = false, action) => { if (serviceWorkerDidSucceed.matches(action)) { - return action.registration; + return true; } return state; }; const checkingForUpdate: Reducer = (state = false, action) => { - if (checkForUpdate.matches(action)) { + if (appCheckForUpdate.matches(action)) { return true; } - if (didCheckForUpdate.matches(action)) { + if (appDidCheckForUpdate.matches(action)) { if (!action.updateFound) { return false; } @@ -94,7 +92,7 @@ const readyForOfflineUse: Reducer = (state = false, action) => { }; export default combineReducers({ - serviceWorker, + isServiceWorkerRegistered, checkingForUpdate, updateAvailable, hasUnresolvedInstallPrompt, diff --git a/src/app/sagas.test.ts b/src/app/sagas.test.ts index d5733e95..180d6f14 100644 --- a/src/app/sagas.test.ts +++ b/src/app/sagas.test.ts @@ -2,19 +2,45 @@ // Copyright (c) 2021-2022 The Pybricks Authors import { createEvent, fireEvent } from '@testing-library/dom'; -import { AsyncSaga, delay } from '../../test'; +import { mock } from 'jest-mock-extended'; +import { AsyncSaga } from '../../test'; +import { + serviceWorkerDidSucceed, + serviceWorkerDidUpdate, +} from '../service-worker/actions'; import { BeforeInstallPromptEvent } from '../utils/dom'; import { + appCheckForUpdate, + appDidCheckForUpdate, appDidReceiveBeforeInstallPrompt, appDidResolveInstallPrompt, + appReload, appShowInstallPrompt, - checkForUpdate, - didCheckForUpdate, didInstall, - reload, } from './actions'; import app from './sagas'; +jest.mock('../serviceWorkerRegistration'); + +/** + * Creates an AsyncSaga initialize with a service worker. + * @param registration The service worker registration. + * @returns The saga. + */ +async function createSagaWithRegistration( + registration: ServiceWorkerRegistration, +): Promise { + const saga = new AsyncSaga(app); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../serviceWorkerRegistration')._fireOnSuccess(registration); + + const action = await saga.take(); + expect(action).toEqual(serviceWorkerDidSucceed()); + + return saga; +} + test('monitorAppInstalled', async () => { const saga = new AsyncSaga(app); @@ -37,13 +63,13 @@ test('monitorBeforeInstallPrompt', async () => { await saga.end(); }); -test('reload', async () => { - const saga = new AsyncSaga(app); - +test('handleAppReload', async () => { // mock registration as if service worker was register on app startup - const registration: Partial = { + const registration = mock({ unregister: jest.fn(), - }; + }); + + const saga = await createSagaWithRegistration(registration); // @ts-expect-error: JSDOM implementation of location.reload() causes error delete window.location; @@ -52,7 +78,7 @@ test('reload', async () => { reload: jest.fn(), }; - saga.put(reload(registration as ServiceWorkerRegistration)); + saga.put(appReload()); expect(registration.unregister).toHaveBeenCalled(); expect(location.reload).toHaveBeenCalled(); @@ -60,26 +86,55 @@ test('reload', async () => { await saga.end(); }); -test('checkForUpdates', async () => { - const saga = new AsyncSaga(app); +describe('handleAppCheckForUpdate', () => { + it('should return true if updates are available', async () => { + // mock registration as if service worker was registered on app startup + const registration = mock({ + update: jest.fn(), + installing: mock(), + }); - // mock registration as if service worker was register on app startup - const registration: Partial = { - update: jest.fn(), - installing: null, - }; + const saga = await createSagaWithRegistration(registration); - saga.put(checkForUpdate(registration as ServiceWorkerRegistration)); + saga.put(appCheckForUpdate()); - // yield to allow generators to complete - await delay(0); + const action = await saga.take(); + expect(action).toStrictEqual(appDidCheckForUpdate(true)); + expect(registration.update).toHaveBeenCalled(); - expect(registration.update).toHaveBeenCalled(); + await saga.end(); + }); - const action = await saga.take(); - expect(action).toStrictEqual(didCheckForUpdate(false)); + it('should return false if no updates are available', async () => { + // mock registration as if service worker was registered on app startup + const registration = mock({ + update: jest.fn(), + installing: null, + }); - await saga.end(); + const saga = await createSagaWithRegistration(registration); + + saga.put(appCheckForUpdate()); + + const action = await saga.take(); + expect(action).toStrictEqual(appDidCheckForUpdate(false)); + expect(registration.update).toHaveBeenCalled(); + + await saga.end(); + }); +}); + +describe('monitorServiceWorkerRegistration', () => { + it('should dispatch serviceWorkerDidUpdate', async () => { + const registration = mock(); + const saga = await createSagaWithRegistration(registration); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../serviceWorkerRegistration')._fireOnUpdate(registration); + + const action = await saga.take(); + expect(action).toEqual(serviceWorkerDidUpdate()); + }); }); test('appShowInstallPrompt', async () => { diff --git a/src/app/sagas.ts b/src/app/sagas.ts index c8c3188e..8242b4fd 100644 --- a/src/app/sagas.ts +++ b/src/app/sagas.ts @@ -3,17 +3,77 @@ import { eventChannel } from 'redux-saga'; import { call, fork, put, take, takeEvery } from 'typed-redux-saga/macro'; +import { + serviceWorkerDidSucceed, + serviceWorkerDidUpdate, +} from '../service-worker/actions'; +import * as serviceWorkerRegistration from '../serviceWorkerRegistration'; import { BeforeInstallPromptEvent } from '../utils/dom'; import { + appCheckForUpdate, + appDidCheckForUpdate, appDidReceiveBeforeInstallPrompt, appDidResolveInstallPrompt, + appReload, appShowInstallPrompt, - checkForUpdate, - didCheckForUpdate, didInstall, - reload, } from './actions'; +/** + * Handles appReload actions. + * + * Must be called (forked) with serviceWorkerRegistration context set. + */ +function* handleAppReload(registration: ServiceWorkerRegistration): Generator { + yield* call(() => registration.unregister()); + + location.reload(); +} + +/** + * Handles appCheckForUpdate actions. + * + * Must be called (forked) with serviceWorkerRegistration context set. + */ +function* handleAppCheckForUpdate(registration: ServiceWorkerRegistration): Generator { + yield* call(() => registration.update()); + const updateFound = registration.installing !== null; + yield* put(appDidCheckForUpdate(updateFound)); +} + +/** + * Marshals CRA serviceWorkerRegistration to saga. + */ +function* monitorServiceWorkerRegistration(): Generator { + const chan = eventChannel<{ + isUpdate: boolean; + registration: ServiceWorkerRegistration; + }>((emit) => { + serviceWorkerRegistration.register({ + onSuccess: (r) => emit({ isUpdate: false, registration: r }), + onUpdate: (r) => emit({ isUpdate: true, registration: r }), + }); + + // istanbul ignore next: never unregistered + return () => serviceWorkerRegistration.unregister(); + }); + + // HACK: is assumed that this will only be called at most two times, once + // with isUpdate === false and after that, once with isUpdate === true. + while (true) { + const { isUpdate, registration } = yield* take(chan); + + if (isUpdate) { + yield* put(serviceWorkerDidUpdate()); + } else { + yield* takeEvery(appReload, handleAppReload, registration); + yield* takeEvery(appCheckForUpdate, handleAppCheckForUpdate, registration); + + yield* put(serviceWorkerDidSucceed()); + } + } +} + function* monitorAppInstalled(): Generator { const chan = eventChannel((emit) => { const listener = (e: Event) => { @@ -61,20 +121,8 @@ function* monitorBeforeInstallPrompt(): Generator { } } -function* handleReload(action: ReturnType): Generator { - yield* call(() => action.registration.unregister()); - location.reload(); -} - -function* handleCheckForUpdate(action: ReturnType): Generator { - yield* call(() => action.registration.update()); - const updateFound = action.registration.installing !== null; - yield* put(didCheckForUpdate(updateFound)); -} - export default function* app(): Generator { + yield* fork(monitorServiceWorkerRegistration); yield* fork(monitorAppInstalled); yield* fork(monitorBeforeInstallPrompt); - yield* takeEvery(reload, handleReload); - yield* takeEvery(checkForUpdate, handleCheckForUpdate); } diff --git a/src/index.tsx b/src/index.tsx index 4d9e587e..813f8b15 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -16,11 +16,6 @@ import * as I18nToaster from './notifications/I18nToaster'; import { rootReducer } from './reducers'; import reportWebVitals from './reportWebVitals'; import rootSaga, { RootSagaContext } from './sagas'; -import { - serviceWorkerDidSucceed, - serviceWorkerDidUpdate, -} from './service-worker/actions'; -import * as serviceWorkerRegistration from './serviceWorkerRegistration'; import { defaultTerminalContext } from './terminal/TerminalContext'; import ViewHeightSensor from './utils/ViewHeightSensor'; import { createCountFunc } from './utils/iter'; @@ -65,14 +60,6 @@ ReactDOM.render( document.getElementById('root'), ); -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://cra.link/PWA -serviceWorkerRegistration.register({ - onUpdate: (r) => store.dispatch(serviceWorkerDidUpdate(r)), - onSuccess: (r) => store.dispatch(serviceWorkerDidSucceed(r)), -}); - // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals diff --git a/src/notifications/sagas.test.ts b/src/notifications/sagas.test.ts index eb7b0180..fdd75d3f 100644 --- a/src/notifications/sagas.test.ts +++ b/src/notifications/sagas.test.ts @@ -9,7 +9,7 @@ import { } from '@pybricks/firmware'; import { AnyAction } from 'redux'; import { AsyncSaga } from '../../test'; -import { didCheckForUpdate } from '../app/actions'; +import { appDidCheckForUpdate } from '../app/actions'; import { bleDIServiceDidReceiveFirmwareRevision } from '../ble-device-info-service/actions'; import { BleDeviceFailToConnectReasonType, @@ -63,7 +63,7 @@ test.each([ didFailToCompile(['reason']), add('warning', 'message'), add('error', 'message', 'url'), - serviceWorkerDidUpdate({} as ServiceWorkerRegistration), + serviceWorkerDidUpdate(), didFailToFinish(FailToFinishReasonType.TimedOut), didFailToFinish( FailToFinishReasonType.BleError, @@ -89,7 +89,7 @@ test.each([ didFailToFinish(FailToFinishReasonType.FailedToCompile), didFailToFinish(FailToFinishReasonType.FirmwareSize), didFailToFinish(FailToFinishReasonType.Unknown, new Error('test error')), - didCheckForUpdate(false), + appDidCheckForUpdate(false), bleDIServiceDidReceiveFirmwareRevision('3.0.0'), didFailToSaveAs(new DOMException('test message', 'NotAllowedError')), fileStorageDidFailToInitialize(new Error('test error')), @@ -125,8 +125,8 @@ test.each([ bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.Canceled }), bootloaderDidFailToConnect(BootloaderConnectionFailureReason.Canceled), didFailToFinish(FailToFinishReasonType.FailedToConnect), - serviceWorkerDidSucceed({} as ServiceWorkerRegistration), - didCheckForUpdate(true), + serviceWorkerDidSucceed(), + appDidCheckForUpdate(true), bleDIServiceDidReceiveFirmwareRevision(firmwareVersion), didFailToSaveAs(new DOMException('test message', 'AbortError')), fileStorageDidFailToExportFile( diff --git a/src/notifications/sagas.ts b/src/notifications/sagas.ts index 0ff7df9a..76447f8a 100644 --- a/src/notifications/sagas.ts +++ b/src/notifications/sagas.ts @@ -10,7 +10,7 @@ import React from 'react'; import { channel } from 'redux-saga'; import * as semver from 'semver'; import { delay, getContext, put, take, takeEvery } from 'typed-redux-saga/macro'; -import { didCheckForUpdate, reload } from '../app/actions'; +import { appDidCheckForUpdate, appReload } from '../app/actions'; import { appName } from '../app/constants'; import { bleDIServiceDidReceiveFirmwareRevision } from '../ble-device-info-service/actions'; import { @@ -31,7 +31,7 @@ import { didFailToConnect as bootloaderDidFailToConnect, } from '../lwp3-bootloader/actions'; import { didCompile, didFailToCompile } from '../mpy/actions'; -import { serviceWorkerDidUpdate as serviceWorkerDidUpdate } from '../service-worker/actions'; +import { serviceWorkerDidUpdate } from '../service-worker/actions'; import { pythonVersionToSemver } from '../utils/version'; import NotificationAction from './NotificationAction'; import NotificationMessage from './NotificationMessage'; @@ -336,9 +336,7 @@ function* handleAddNotification(action: ReturnType): Gen }); } -function* showServiceWorkerUpdate( - action: ReturnType, -): Generator { +function* showServiceWorkerUpdate(): Generator { const ch = channel>(); const userAction = dispatchAction( MessageId.ServiceWorkerUpdateAction, @@ -358,10 +356,10 @@ function* showServiceWorkerUpdate( yield* take(ch); - yield* put(reload(action.registration)); + yield* put(appReload()); } -function* showNoUpdateInfo(action: ReturnType): Generator { +function* showNoUpdateInfo(action: ReturnType): Generator { if (action.updateFound) { // this will be handled by serviceWorkerDidUpdate action return; @@ -442,7 +440,7 @@ export default function* (): Generator { yield* takeEvery(didFailToCompile, showCompilerError); yield* takeEvery(addNotification, handleAddNotification); yield* takeEvery(serviceWorkerDidUpdate, showServiceWorkerUpdate); - yield* takeEvery(didCheckForUpdate, showNoUpdateInfo); + yield* takeEvery(appDidCheckForUpdate, showNoUpdateInfo); yield* takeEvery(bleDIServiceDidReceiveFirmwareRevision, checkVersion); yield* takeEvery(fileStorageDidFailToInitialize, showFileStorageFailToInitialize); yield* takeEvery(fileStorageDidFailToReadFile, showFileStorageFailToRead); diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts index ac2c2814..4350f863 100644 --- a/src/reportWebVitals.ts +++ b/src/reportWebVitals.ts @@ -1,3 +1,5 @@ +// istanbul ignore file + import { ReportHandler } from 'web-vitals'; const reportWebVitals = (onPerfEntry?: ReportHandler): void => { diff --git a/src/service-worker.ts b/src/service-worker.ts index d92932cd..75c64997 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -8,6 +8,8 @@ // You can also remove this file if you'd prefer not to use a // service worker, and the Workbox build step will be skipped. +// istanbul ignore file + import { clientsClaim } from 'workbox-core'; import { ExpirationPlugin } from 'workbox-expiration'; import { createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching'; diff --git a/src/service-worker/actions.ts b/src/service-worker/actions.ts index b0e27394..370e1fcd 100644 --- a/src/service-worker/actions.ts +++ b/src/service-worker/actions.ts @@ -3,16 +3,10 @@ import { createAction } from '../actions'; -export const serviceWorkerDidUpdate = createAction( - (registration: ServiceWorkerRegistration) => ({ - type: 'serviceWorker.action.didUpdate', - registration, - }), -); +export const serviceWorkerDidUpdate = createAction(() => ({ + type: 'serviceWorker.action.didUpdate', +})); -export const serviceWorkerDidSucceed = createAction( - (registration: ServiceWorkerRegistration) => ({ - type: 'serviceWorker.action.didSucceed', - registration, - }), -); +export const serviceWorkerDidSucceed = createAction(() => ({ + type: 'serviceWorker.action.didSucceed', +})); diff --git a/src/serviceWorkerRegistration.ts b/src/serviceWorkerRegistration.ts index b2323922..c377b777 100644 --- a/src/serviceWorkerRegistration.ts +++ b/src/serviceWorkerRegistration.ts @@ -10,6 +10,8 @@ // To learn more about the benefits of this model and instructions on how to // opt-in, read https://cra.link/PWA +// istanbul ignore file + const isLocalhost = Boolean( window.location.hostname === 'localhost' || // [::1] is the IPv6 localhost address. diff --git a/src/settings/SettingsDrawer.tsx b/src/settings/SettingsDrawer.tsx index 9590c693..f115bc7b 100644 --- a/src/settings/SettingsDrawer.tsx +++ b/src/settings/SettingsDrawer.tsx @@ -21,7 +21,7 @@ import { useI18n } from '@shopify/react-i18n'; import React, { useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import AboutDialog from '../about/AboutDialog'; -import { appShowInstallPrompt, checkForUpdate, reload } from '../app/actions'; +import { appCheckForUpdate, appReload, appShowInstallPrompt } from '../app/actions'; import { pybricksBugReportsUrl, pybricksGitterUrl, @@ -50,7 +50,9 @@ const SettingsDrawer: React.FunctionComponent = (props) => { const showDocs = useSelector((s) => s.settings.showDocs); const darkMode = useSelector((s) => s.settings.darkMode); const flashCurrentProgram = useSelector((s) => s.settings.flashCurrentProgram); - const serviceWorker = useSelector((s) => s.app.serviceWorker); + const isServiceWorkerRegistered = useSelector( + (s) => s.app.isServiceWorkerRegistered, + ); const checkingForUpdate = useSelector((s) => s.app.checkingForUpdate); const updateAvailable = useSelector((s) => s.app.updateAvailable); const hasUnresolvedInstallPrompt = useSelector( @@ -290,12 +292,10 @@ const SettingsDrawer: React.FunctionComponent = (props) => { {i18n.translate(SettingsStringId.AppInstallLabel)} )} - {serviceWorker && !updateAvailable && ( + {isServiceWorkerRegistered && !updateAvailable && ( )} - {serviceWorker && updateAvailable && ( + {isServiceWorkerRegistered && updateAvailable && (