Add error toast when service worker registration fails (#29895)
* Add error toast when service worker registration fails Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
committed by
GitHub
parent
e427b71040
commit
0f783ede5e
@@ -72,7 +72,7 @@ export default abstract class BasePlatform {
|
|||||||
protected _favicon?: Favicon;
|
protected _favicon?: Favicon;
|
||||||
|
|
||||||
protected constructor() {
|
protected constructor() {
|
||||||
dis.register(this.onAction);
|
dis.register(this.onAction.bind(this));
|
||||||
this.startUpdateCheck = this.startUpdateCheck.bind(this);
|
this.startUpdateCheck = this.startUpdateCheck.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,14 +85,14 @@ export default abstract class BasePlatform {
|
|||||||
*/
|
*/
|
||||||
public abstract getDefaultDeviceDisplayName(): string;
|
public abstract getDefaultDeviceDisplayName(): string;
|
||||||
|
|
||||||
protected onAction = (payload: ActionPayload): void => {
|
protected onAction(payload: ActionPayload): void {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case "on_client_not_viable":
|
case "on_client_not_viable":
|
||||||
case Action.OnLoggedOut:
|
case Action.OnLoggedOut:
|
||||||
this.setNotificationCount(0);
|
this.setNotificationCount(0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Used primarily for Analytics
|
// Used primarily for Analytics
|
||||||
public abstract getHumanReadableName(): string;
|
public abstract getHumanReadableName(): string;
|
||||||
|
|||||||
@@ -2457,6 +2457,10 @@
|
|||||||
"recent_changes_heading": "Recent changes that have not yet been received",
|
"recent_changes_heading": "Recent changes that have not yet been received",
|
||||||
"title": "Server isn't responding"
|
"title": "Server isn't responding"
|
||||||
},
|
},
|
||||||
|
"service_worker_error": {
|
||||||
|
"description": "%(brand)s requires a service worker for loading authenticated media from Matrix content repositories. This is not supported by your browser so you may experience media failing to load.",
|
||||||
|
"title": "Failed to load service worker"
|
||||||
|
},
|
||||||
"seshat": {
|
"seshat": {
|
||||||
"error_initialising": "Message search initialisation failed, check <a>your settings</a> for more information",
|
"error_initialising": "Message search initialisation failed, check <a>your settings</a> for more information",
|
||||||
"reset_button": "Reset event store",
|
"reset_button": "Reset event store",
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ import { Action } from "../../dispatcher/actions";
|
|||||||
import { type CheckUpdatesPayload } from "../../dispatcher/payloads/CheckUpdatesPayload";
|
import { type CheckUpdatesPayload } from "../../dispatcher/payloads/CheckUpdatesPayload";
|
||||||
import { parseQs } from "../url_utils";
|
import { parseQs } from "../url_utils";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
|
import ToastStore from "../../stores/ToastStore.ts";
|
||||||
|
import GenericToast from "../../components/views/toasts/GenericToast.tsx";
|
||||||
|
import SdkConfig from "../../SdkConfig.ts";
|
||||||
|
import type { ActionPayload } from "../../dispatcher/payloads.ts";
|
||||||
|
|
||||||
const POKE_RATE_MS = 10 * 60 * 1000; // 10 min
|
const POKE_RATE_MS = 10 * 60 * 1000; // 10 min
|
||||||
|
|
||||||
@@ -32,32 +36,59 @@ function getNormalizedAppVersion(version: string): string {
|
|||||||
|
|
||||||
export default class WebPlatform extends BasePlatform {
|
export default class WebPlatform extends BasePlatform {
|
||||||
private static readonly VERSION = process.env.VERSION!; // baked in by Webpack
|
private static readonly VERSION = process.env.VERSION!; // baked in by Webpack
|
||||||
|
private readonly registerServiceWorkerPromise: Promise<void>;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// Register the service worker in the background
|
// Register the service worker in the background
|
||||||
this.tryRegisterServiceWorker().catch((e) => console.error("Error registering/updating service worker:", e));
|
this.registerServiceWorkerPromise = this.registerServiceWorker();
|
||||||
|
this.registerServiceWorkerPromise.catch((e) => {
|
||||||
|
console.error("Error registering/updating service worker:", e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tryRegisterServiceWorker(): Promise<void> {
|
protected onAction(payload: ActionPayload): void {
|
||||||
if (!("serviceWorker" in navigator)) {
|
super.onAction(payload);
|
||||||
return; // not available on this platform - don't try to register the service worker
|
|
||||||
|
switch (payload.action) {
|
||||||
|
case "client_started":
|
||||||
|
// Defer drawing the toast until the client is started as the lifecycle methods reset the ToastStore right before
|
||||||
|
this.registerServiceWorkerPromise.catch(this.handleServiceWorkerRegistrationError);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async registerServiceWorker(): Promise<void> {
|
||||||
// sw.js is exported by webpack, sourced from `/src/serviceworker/index.ts`
|
// sw.js is exported by webpack, sourced from `/src/serviceworker/index.ts`
|
||||||
const registration = await navigator.serviceWorker.register("sw.js");
|
const registration = await navigator.serviceWorker.register("sw.js");
|
||||||
if (!registration) {
|
if (!registration) {
|
||||||
// Registration didn't work for some reason - assume failed and ignore.
|
throw new Error("Service worker registration failed");
|
||||||
// This typically happens in Jest.
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.serviceWorker.addEventListener("message", this.onServiceWorkerPostMessage.bind(this));
|
navigator.serviceWorker.addEventListener("message", this.onServiceWorkerPostMessage);
|
||||||
await registration.update();
|
await registration.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onServiceWorkerPostMessage(event: MessageEvent): void {
|
private handleServiceWorkerRegistrationError = (): void => {
|
||||||
|
const key = "service_worker_error";
|
||||||
|
const brand = SdkConfig.get().brand;
|
||||||
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
|
key,
|
||||||
|
title: _t("service_worker_error|title"),
|
||||||
|
props: {
|
||||||
|
description: _t("service_worker_error|description", { brand }),
|
||||||
|
primaryLabel: _t("action|ok"),
|
||||||
|
onPrimaryClick: () => {
|
||||||
|
ToastStore.sharedInstance().dismissToast(key);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
component: GenericToast,
|
||||||
|
priority: 95,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onServiceWorkerPostMessage = (event: MessageEvent): void => {
|
||||||
try {
|
try {
|
||||||
if (event.data?.["type"] === "userinfo" && event.data?.["responseKey"]) {
|
if (event.data?.["type"] === "userinfo" && event.data?.["responseKey"]) {
|
||||||
const userId = localStorage.getItem("mx_user_id");
|
const userId = localStorage.getItem("mx_user_id");
|
||||||
@@ -73,7 +104,7 @@ export default class WebPlatform extends BasePlatform {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error responding to service worker: ", e);
|
console.error("Error responding to service worker: ", e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public getHumanReadableName(): string {
|
public getHumanReadableName(): string {
|
||||||
return "Web Platform"; // no translation required: only used for analytics
|
return "Web Platform"; // no translation required: only used for analytics
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import { UpdateCheckStatus } from "../../../../src/BasePlatform";
|
|||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import WebPlatform from "../../../../src/vector/platform/WebPlatform";
|
import WebPlatform from "../../../../src/vector/platform/WebPlatform";
|
||||||
import { setupLanguageMock } from "../../../setup/setupLanguage";
|
import { setupLanguageMock } from "../../../setup/setupLanguage";
|
||||||
|
import ToastStore from "../../../../src/stores/ToastStore.ts";
|
||||||
|
import defaultDispatcher from "../../../../src/dispatcher/dispatcher.ts";
|
||||||
|
import { emitPromise } from "../../../test-utils";
|
||||||
|
|
||||||
fetchMock.config.overwriteRoutes = true;
|
fetchMock.config.overwriteRoutes = true;
|
||||||
|
|
||||||
@@ -26,13 +29,34 @@ describe("WebPlatform", () => {
|
|||||||
expect(platform.getHumanReadableName()).toEqual("Web Platform");
|
expect(platform.getHumanReadableName()).toEqual("Web Platform");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("registers service worker", () => {
|
describe("service worker", () => {
|
||||||
// @ts-ignore - mocking readonly object
|
it("registers successfully", () => {
|
||||||
navigator.serviceWorker = { register: jest.fn() };
|
// @ts-expect-error - mocking readonly object
|
||||||
|
navigator.serviceWorker = {
|
||||||
|
register: jest.fn().mockResolvedValue({
|
||||||
|
update: jest.fn(),
|
||||||
|
}),
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
};
|
||||||
new WebPlatform();
|
new WebPlatform();
|
||||||
expect(navigator.serviceWorker.register).toHaveBeenCalled();
|
expect(navigator.serviceWorker.register).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("handles errors", async () => {
|
||||||
|
// @ts-expect-error - mocking readonly object
|
||||||
|
navigator.serviceWorker = {
|
||||||
|
register: undefined,
|
||||||
|
};
|
||||||
|
new WebPlatform();
|
||||||
|
|
||||||
|
defaultDispatcher.dispatch({ action: "client_started" });
|
||||||
|
await emitPromise(ToastStore.sharedInstance(), "update");
|
||||||
|
const toasts = ToastStore.sharedInstance().getToasts();
|
||||||
|
expect(toasts).toHaveLength(1);
|
||||||
|
expect(toasts[0].title).toEqual("Failed to load service worker");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should call reload on window location object", () => {
|
it("should call reload on window location object", () => {
|
||||||
Object.defineProperty(window, "location", { value: { reload: jest.fn() }, writable: true });
|
Object.defineProperty(window, "location", { value: { reload: jest.fn() }, writable: true });
|
||||||
|
|
||||||
@@ -146,10 +170,7 @@ describe("WebPlatform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("pollForUpdate()", () => {
|
describe("pollForUpdate()", () => {
|
||||||
it(
|
it("should return not available and call showNoUpdate when current version matches most recent version", async () => {
|
||||||
"should return not available and call showNoUpdate when current version " +
|
|
||||||
"matches most recent version",
|
|
||||||
async () => {
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
WebPlatform.VERSION = prodVersion;
|
WebPlatform.VERSION = prodVersion;
|
||||||
fetchMock.getOnce("/version", prodVersion);
|
fetchMock.getOnce("/version", prodVersion);
|
||||||
@@ -162,8 +183,7 @@ describe("WebPlatform", () => {
|
|||||||
expect(result).toEqual({ status: UpdateCheckStatus.NotAvailable });
|
expect(result).toEqual({ status: UpdateCheckStatus.NotAvailable });
|
||||||
expect(showUpdate).not.toHaveBeenCalled();
|
expect(showUpdate).not.toHaveBeenCalled();
|
||||||
expect(showNoUpdate).toHaveBeenCalled();
|
expect(showNoUpdate).toHaveBeenCalled();
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it("should strip v prefix from versions before comparing", async () => {
|
it("should strip v prefix from versions before comparing", async () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
Reference in New Issue
Block a user