diff --git a/CHANGELOG.md b/CHANGELOG.md
index f21d932d..545218cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@
## [Unreleased]
+### Fixed
+- Fixed app freezing when checking for updates and update server is unreachable ([pybricks-code#1299]).
+
+[pybricks-code#1299]: https://github.com/pybricks/pybricks-code/issues/1299
+
## [2.0.0-beta.10] - 2022-11-11
### Added
diff --git a/src/alerts.ts b/src/alerts.ts
index 318fd175..3bc2a0e1 100644
--- a/src/alerts.ts
+++ b/src/alerts.ts
@@ -3,6 +3,7 @@
import { ToastProps } from '@blueprintjs/core';
import alerts from './alerts/alerts';
+import app from './app/alerts';
import ble from './ble/alerts';
import explorer from './explorer/alerts';
import firmware from './firmware/alerts';
@@ -12,6 +13,7 @@ import type { CreateToast } from './toasterTypes';
/** This collects alerts from all of the subsystems of the app */
const alertDomains = {
alerts,
+ app,
ble,
explorer,
firmware,
diff --git a/src/app/actions.ts b/src/app/actions.ts
index 546c5239..540502c0 100644
--- a/src/app/actions.ts
+++ b/src/app/actions.ts
@@ -21,6 +21,11 @@ export const appDidCheckForUpdate = createAction((updateFound: boolean) => ({
updateFound,
}));
+/** Action that indicates that checking for an update failed. */
+export const appDidFailToCheckForUpdate = createAction(() => ({
+ type: 'app.action.didFailToCheckForUpdate',
+}));
+
/* Action that indicates the browser wants to prompt the use to install the app. */
export const appDidReceiveBeforeInstallPrompt = createAction(() => ({
type: 'app.action.didBeforeInstallPrompt',
diff --git a/src/app/alerts/UpdateServerFailure.tsx b/src/app/alerts/UpdateServerFailure.tsx
new file mode 100644
index 00000000..0abf2b2d
--- /dev/null
+++ b/src/app/alerts/UpdateServerFailure.tsx
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+import { Intent } from '@blueprintjs/core';
+import React from 'react';
+import type { CreateToast } from '../../toasterTypes';
+import { useI18n } from './i18n';
+
+const UpdateServerFailure: React.VoidFunctionComponent = () => {
+ const i18n = useI18n();
+ return
{i18n.translate('updateServerFailure.message')}
;
+};
+
+export const updateServerFailure: CreateToast = (onAction) => {
+ return {
+ message: ,
+ icon: 'error',
+ intent: Intent.DANGER,
+ onDismiss: () => onAction('dismiss'),
+ };
+};
diff --git a/src/app/alerts/i18n.ts b/src/app/alerts/i18n.ts
new file mode 100644
index 00000000..eb8dc486
--- /dev/null
+++ b/src/app/alerts/i18n.ts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+import { useI18n as useShopifyI18n } from '@shopify/react-i18n';
+import type { TypedI18n } from '../../i18n';
+import type translations from './translations/en.json';
+
+export function useI18n(): TypedI18n {
+ // istanbul ignore next: babel-loader rewrites this line
+ const [i18n] = useShopifyI18n();
+ return i18n;
+}
diff --git a/src/app/alerts/index.ts b/src/app/alerts/index.ts
new file mode 100644
index 00000000..35084e53
--- /dev/null
+++ b/src/app/alerts/index.ts
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+import { updateServerFailure } from './UpdateServerFailure';
+
+// gathers all of the alert creation functions for passing up to the top level
+export default {
+ updateServerFailure,
+};
diff --git a/src/app/alerts/translations/en.json b/src/app/alerts/translations/en.json
new file mode 100644
index 00000000..bd914faa
--- /dev/null
+++ b/src/app/alerts/translations/en.json
@@ -0,0 +1,5 @@
+{
+ "updateServerFailure": {
+ "message": "Failed to connect to update server. The Internet connection or the server may be down. Try again later."
+ }
+}
diff --git a/src/app/reducers.test.ts b/src/app/reducers.test.ts
index 0b7b5629..0cb38745 100644
--- a/src/app/reducers.test.ts
+++ b/src/app/reducers.test.ts
@@ -9,6 +9,7 @@ import {
import {
appCheckForUpdate,
appDidCheckForUpdate,
+ appDidFailToCheckForUpdate,
appDidReceiveBeforeInstallPrompt,
appDidResolveInstallPrompt,
appShowInstallPrompt,
@@ -59,6 +60,10 @@ test('checkingForUpdate', () => {
reducers({ checkingForUpdate: true } as State, appDidCheckForUpdate(false))
.checkingForUpdate,
).toBe(false);
+ expect(
+ reducers({ checkingForUpdate: true } as State, appDidFailToCheckForUpdate())
+ .checkingForUpdate,
+ ).toBe(false);
expect(
reducers({ checkingForUpdate: true } as State, serviceWorkerDidUpdate())
.checkingForUpdate,
diff --git a/src/app/reducers.ts b/src/app/reducers.ts
index 7b450fb4..0849e4f8 100644
--- a/src/app/reducers.ts
+++ b/src/app/reducers.ts
@@ -11,6 +11,7 @@ import {
import {
appCheckForUpdate,
appDidCheckForUpdate,
+ appDidFailToCheckForUpdate,
appDidReceiveBeforeInstallPrompt,
appDidResolveInstallPrompt,
appShowInstallPrompt,
@@ -39,6 +40,10 @@ const checkingForUpdate: Reducer = (state = false, action) => {
return state;
}
+ if (appDidFailToCheckForUpdate.matches(action)) {
+ return false;
+ }
+
if (serviceWorkerDidUpdate.matches(action)) {
return false;
}
diff --git a/src/app/sagas.ts b/src/app/sagas.ts
index 31ee6393..db9c0515 100644
--- a/src/app/sagas.ts
+++ b/src/app/sagas.ts
@@ -3,15 +3,18 @@
import { eventChannel } from 'redux-saga';
import { call, delay, fork, put, take, takeEvery } from 'typed-redux-saga/macro';
+import { alertsShowAlert } from '../alerts/actions';
import {
serviceWorkerDidSucceed,
serviceWorkerDidUpdate,
} from '../service-worker/actions';
import * as serviceWorkerRegistration from '../serviceWorkerRegistration';
+import { ensureError } from '../utils';
import { BeforeInstallPromptEvent } from '../utils/dom';
import {
appCheckForUpdate,
appDidCheckForUpdate,
+ appDidFailToCheckForUpdate,
appDidReceiveBeforeInstallPrompt,
appDidResolveInstallPrompt,
appReload,
@@ -36,9 +39,23 @@ function* handleAppReload(registration: ServiceWorkerRegistration): Generator {
* 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));
+ try {
+ yield* call(() => registration.update());
+ const updateFound = registration.installing !== null;
+ yield* put(appDidCheckForUpdate(updateFound));
+ } catch (err) {
+ if (err instanceof TypeError) {
+ yield* put(alertsShowAlert('app', 'updateServerFailure'));
+ } else {
+ yield* put(
+ alertsShowAlert('alerts', 'unexpectedError', {
+ error: ensureError(err),
+ }),
+ );
+ }
+
+ yield* put(appDidFailToCheckForUpdate());
+ }
}
/**