Skip to content

Commit

Permalink
fix: app updates are automatically applied if no unsaved changes are …
Browse files Browse the repository at this point in the history
…present (#2181)
  • Loading branch information
TheSlimvReal authored and sleidig committed Jan 22, 2024
1 parent c5de85a commit b090c96
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("ChangelogComponent", () => {
{
provide: UpdateManagerService,
useValue: jasmine.createSpyObj([
"notifyUserWhenUpdateAvailable",
"listenToAppUpdates",
"regularlyCheckForUpdates",
"detectUnrecoverableState",
]),
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/ui/latest-changes/latest-changes.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import { MatButtonModule } from "@angular/material/button";
})
export class LatestChangesModule {
constructor(private updateManagerService: UpdateManagerService) {
this.updateManagerService.notifyUserWhenUpdateAvailable();
this.updateManagerService.listenToAppUpdates();
this.updateManagerService.regularlyCheckForUpdates();
this.updateManagerService.detectUnrecoverableState();
}
Expand Down
70 changes: 43 additions & 27 deletions src/app/core/ui/latest-changes/update-manager.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MatSnackBar } from "@angular/material/snack-bar";
import { LatestChangesDialogService } from "./latest-changes-dialog.service";
import { Subject } from "rxjs";
import { LoggingService } from "../../logging/logging.service";
import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service";

describe("UpdateManagerService", () => {
let service: UpdateManagerService;
Expand All @@ -23,6 +24,7 @@ describe("UpdateManagerService", () => {
let stableSubject: Subject<boolean>;
let latestChangesDialog: jasmine.SpyObj<LatestChangesDialogService>;
let mockLogger: jasmine.SpyObj<LoggingService>;
let unsavedChanges: UnsavedChangesService;

beforeEach(() => {
mockLocation = jasmine.createSpyObj(["reload"]);
Expand All @@ -43,64 +45,80 @@ describe("UpdateManagerService", () => {
appRef = jasmine.createSpyObj([], { isStable: stableSubject });
latestChangesDialog = jasmine.createSpyObj(["showLatestChangesIfUpdated"]);
mockLogger = jasmine.createSpyObj(["error"]);
unsavedChanges = new UnsavedChangesService(undefined);
unsavedChanges.pending = true;

service = createService();
});

afterEach(() => localStorage.clear());

it("should create", () => {
expect(service).toBeTruthy();
});

it("should show a snackBar that allows to reload the page when an update is available", fakeAsync(() => {
service.notifyUserWhenUpdateAvailable();
it("should show a snackBar that allows to reload the page when an update is available", () => {
service.listenToAppUpdates();
// notify about new update
updateSubject.next({ type: "VERSION_READY" });
tick();

expect(snackBar.open).toHaveBeenCalled();

// user activates update
snackBarAction.next(undefined);
tick();

expect(mockLocation.reload).toHaveBeenCalled();
}));
});

it("should reload app if no unsaved changes are detected", () => {
service.listenToAppUpdates();
unsavedChanges.pending = true;

updateSubject.next({ type: "VERSION_READY" });

expect(mockLocation.reload).not.toHaveBeenCalled();
expect(snackBar.open).toHaveBeenCalled();

createService();
unsavedChanges.pending = false;

updateSubject.next({ type: "VERSION_READY" });

expect(mockLocation.reload).toHaveBeenCalled();
});

it("should reload the page during construction if noted in the local storage", () => {
const version = "1.1.1";
window.localStorage.setItem(
localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
"update-" + version,
);

createService();

expect(mockLocation.reload).toHaveBeenCalled();
expect(
window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY),
).toBe(version);
expect(localStorage.getItem(LatestChangesDialogService.VERSION_KEY)).toBe(
version,
);
});

it("should set the note for reloading the app on next startup and remove it if user triggers reload manually", fakeAsync(() => {
it("should set the note for reloading the app on next startup and remove it if user triggers reload manually", () => {
const version = "1.1.1";
window.localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
version,
);
service.notifyUserWhenUpdateAvailable();
localStorage.setItem(LatestChangesDialogService.VERSION_KEY, version);
service.listenToAppUpdates();
updateSubject.next({ type: "VERSION_READY" });

expect(
window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY),
).toBe("update-" + version);
expect(localStorage.getItem(LatestChangesDialogService.VERSION_KEY)).toBe(
"update-" + version,
);

// reload is triggered by clicking button on the snackbar
snackBarAction.next();

expect(
window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY),
).toBe(version);
}));
expect(localStorage.getItem(LatestChangesDialogService.VERSION_KEY)).toBe(
version,
);
});

it("should check for updates once on startup and then every hour", fakeAsync(() => {
service.regularlyCheckForUpdates();
Expand Down Expand Up @@ -138,7 +156,7 @@ describe("UpdateManagerService", () => {
it("should trigger the latest changes dialog on startup only if update note is set", () => {
latestChangesDialog.showLatestChangesIfUpdated.calls.reset();

window.localStorage.setItem(
localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
"update-1.0.0",
);
Expand All @@ -148,10 +166,7 @@ describe("UpdateManagerService", () => {
latestChangesDialog.showLatestChangesIfUpdated,
).not.toHaveBeenCalled();

window.localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
"1.0.0",
);
localStorage.setItem(LatestChangesDialogService.VERSION_KEY, "1.0.0");
createService();

expect(latestChangesDialog.showLatestChangesIfUpdated).toHaveBeenCalled();
Expand All @@ -176,6 +191,7 @@ describe("UpdateManagerService", () => {
snackBar,
mockLogger,
latestChangesDialog,
unsavedChanges,
mockLocation,
);
}
Expand Down
54 changes: 30 additions & 24 deletions src/app/core/ui/latest-changes/update-manager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { MatSnackBar } from "@angular/material/snack-bar";
import { LoggingService } from "../../logging/logging.service";
import { LatestChangesDialogService } from "./latest-changes-dialog.service";
import { LOCATION_TOKEN } from "../../../utils/di-tokens";
import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service";

/**
* Check with the server whether a new version of the app is available in order to notify the user.
Expand All @@ -41,17 +42,18 @@ export class UpdateManagerService {
private snackBar: MatSnackBar,
private logger: LoggingService,
private latestChangesDialogService: LatestChangesDialogService,
private unsavedChanges: UnsavedChangesService,
@Inject(LOCATION_TOKEN) private location: Location,
) {
this.updates.unrecoverable.subscribe((err) => {
this.logger.error("App is in unrecoverable state: " + err.reason);
this.location.reload();
});
const currentVersion = window.localStorage.getItem(
const currentVersion = localStorage.getItem(
LatestChangesDialogService.VERSION_KEY,
);
if (currentVersion && currentVersion.startsWith(this.UPDATE_PREFIX)) {
window.localStorage.setItem(
localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
currentVersion.replace(this.UPDATE_PREFIX, ""),
);
Expand All @@ -64,13 +66,13 @@ export class UpdateManagerService {
/**
* Display a notification to the user in case a new app version is detected by the ServiceWorker.
*/
public notifyUserWhenUpdateAvailable() {
public listenToAppUpdates() {
if (!this.updates.isEnabled) {
return;
}
this.updates.versionUpdates
.pipe(filter((e) => e.type === "VERSION_READY"))
.subscribe(() => this.showUpdateNotification());
.subscribe(() => this.updateIfPossible());
}

/**
Expand All @@ -93,33 +95,37 @@ export class UpdateManagerService {
);
}

private showUpdateNotification() {
private updateIfPossible() {
const currentVersion =
window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY) || "";
localStorage.getItem(LatestChangesDialogService.VERSION_KEY) || "";
if (currentVersion.startsWith(this.UPDATE_PREFIX)) {
// Sometimes this is triggered multiple times for one update
return;
}

window.localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
this.UPDATE_PREFIX + currentVersion,
);

this.snackBar
.open(
$localize`A new version of the app is available!`,
$localize`:Action that a user can update the app with:Update`,
)
.onAction()
.subscribe(() => {
window.localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
currentVersion,
);
if (this.unsavedChanges.pending) {
// app cannot be safely reloaded
localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
this.UPDATE_PREFIX + currentVersion,
);
this.snackBar
.open(
$localize`A new version of the app is available!`,
$localize`:Action that a user can update the app with:Update`,
)
.onAction()
.subscribe(() => {
localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
currentVersion,
);

this.location.reload();
});
this.location.reload();
});
} else {
this.location.reload();
}
}

/**
Expand Down

0 comments on commit b090c96

Please sign in to comment.