Skip to content

Commit

Permalink
feat: add custom dialog option (#126)
Browse files Browse the repository at this point in the history
* feat: add custom dialog option

* chore: add missing semicolons

* feat: add onNotifyUser hook and makeUserNotifier

* fix: Import Event interface from electron

* beep boop

* add tests to makeUserNotifier

* chore: minor tweaks

---------

Co-authored-by: Erick Zhao <[email protected]>
Co-authored-by: David Sanders <[email protected]>
  • Loading branch information
3 people authored Dec 13, 2024
1 parent 7e9950e commit 691ad17
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 22 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
coverage
node_modules
dist
.eslintcache
15 changes: 13 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
// @ts-check

import globals from 'globals';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';

export default [eslint.configs.recommended, eslintConfigPrettier, ...tseslint.configs.recommended];
export default [
eslint.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
languageOptions: {
globals: {
...globals.node,
},
},
},
];
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"electron": "^33.2.1",
"eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"globals": "^15.13.0",
"husky": "^9.1.7",
"jest": "^29.0.0",
"lint-staged": "^15.2.10",
Expand Down
112 changes: 98 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import os from 'node:os';
import path from 'node:path';
import { format } from 'node:util';

import { app, autoUpdater, dialog } from 'electron';
import { app, autoUpdater, dialog, Event } from 'electron';

export interface ILogger {
log(message: string): void;
Expand Down Expand Up @@ -46,6 +46,37 @@ export interface IStaticUpdateSource {

export type IUpdateSource = IElectronUpdateServiceSource | IStaticUpdateSource;

export interface IUpdateInfo {
event: Event;
releaseNotes: string;
releaseName: string;
releaseDate: Date;
updateURL: string;
}

export interface IUpdateDialogStrings {
/**
* @param {String} title The title of the dialog box.
* Defaults to `Application Update`
*/
title?: string;
/**
* @param {String} detail The text of the dialog box.
* Defaults to `A new version has been downloaded. Restart the application to apply the updates.`
*/
detail?: string;
/**
* @param {String} restartButtonText The text of the restart button.
* Defaults to `Restart`
*/
restartButtonText?: string;
/**
* @param {String} laterButtonText The text of the later button.
* Defaults to `Later`
*/
laterButtonText?: string;
}

export interface IUpdateElectronAppOptions<L = ILogger> {
/**
* @param {String} repo A GitHub repository in the format `owner/repo`.
Expand Down Expand Up @@ -75,6 +106,13 @@ export interface IUpdateElectronAppOptions<L = ILogger> {
* prompted to apply the update immediately after download.
*/
readonly notifyUser?: boolean;
/**
* Optional callback that replaces the default user prompt dialog whenever the 'update-downloaded' event
* is fired. Only runs if {@link notifyUser} is `true`.
*
* @param info - Information pertaining to the available update.
*/
readonly onNotifyUser?: (info: IUpdateInfo) => void;
}

// eslint-disable-next-line @typescript-eslint/no-require-imports
Expand Down Expand Up @@ -181,17 +219,23 @@ function initUpdater(opts: ReturnType<typeof validateInput>) {
(event, releaseNotes, releaseName, releaseDate, updateURL) => {
log('update-downloaded', [event, releaseNotes, releaseName, releaseDate, updateURL]);

const dialogOpts: Electron.MessageBoxOptions = {
type: 'info',
buttons: ['Restart', 'Later'],
title: 'Application Update',
message: process.platform === 'win32' ? releaseNotes : releaseName,
detail:
'A new version has been downloaded. Restart the application to apply the updates.',
};

dialog.showMessageBox(dialogOpts).then(({ response }) => {
if (response === 0) autoUpdater.quitAndInstall();
if (typeof opts.onNotifyUser !== 'function') {
assert(
opts.onNotifyUser === undefined,
'onNotifyUser option must be a callback function or undefined',
);
log('update-downloaded: notifyUser is true, opening default dialog');
opts.onNotifyUser = makeUserNotifier();
} else {
log('update-downloaded: notifyUser is true, running custom onNotifyUser callback');
}

opts.onNotifyUser({
event,
releaseNotes,
releaseDate,
releaseName,
updateURL,
});
},
);
Expand All @@ -204,6 +248,41 @@ function initUpdater(opts: ReturnType<typeof validateInput>) {
}, ms(updateInterval));
}

/**
* Helper function that generates a callback for use with {@link IUpdateElectronAppOptions.onNotifyUser}.
*
* @param dialogProps - Text to display in the dialog.
*/
export function makeUserNotifier(dialogProps?: IUpdateDialogStrings): (info: IUpdateInfo) => void {
const defaultDialogMessages = {
title: 'Application Update',
detail: 'A new version has been downloaded. Restart the application to apply the updates.',
restartButtonText: 'Restart',
laterButtonText: 'Later',
};

const assignedDialog = Object.assign({}, defaultDialogMessages, dialogProps);

return (info: IUpdateInfo) => {
const { releaseNotes, releaseName } = info;
const { title, restartButtonText, laterButtonText, detail } = assignedDialog;

const dialogOpts: Electron.MessageBoxOptions = {
type: 'info',
buttons: [restartButtonText, laterButtonText],
title,
message: process.platform === 'win32' ? releaseNotes : releaseName,
detail,
};

dialog.showMessageBox(dialogOpts).then(({ response }) => {
if (response === 0) {
autoUpdater.quitAndInstall();
}
});
};
}

function guessRepo() {
const pkgBuf = fs.readFileSync(path.join(app.getAppPath(), 'package.json'));
const pkg = JSON.parse(pkgBuf.toString());
Expand All @@ -220,7 +299,12 @@ function validateInput(opts: IUpdateElectronAppOptions) {
logger: console,
notifyUser: true,
};
const { host, updateInterval, logger, notifyUser } = Object.assign({}, defaults, opts);

const { host, updateInterval, logger, notifyUser, onNotifyUser } = Object.assign(
{},
defaults,
opts,
);

let updateSource = opts.updateSource;
// Handle migration from old properties + default to update service
Expand Down Expand Up @@ -260,5 +344,5 @@ function validateInput(opts: IUpdateElectronAppOptions) {

assert(logger && typeof logger.log, 'function');

return { updateSource, updateInterval, logger, notifyUser };
return { updateSource, updateInterval, logger, notifyUser, onNotifyUser };
}
5 changes: 2 additions & 3 deletions test/__mocks__/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ module.exports = {
setFeedURL: () => {
/* no-op */
},
quitAndInstall: jest.fn(),
},
dialog: {
showMessageBox: () => {
/* no-op */
},
showMessageBox: jest.fn(),
},
};
64 changes: 61 additions & 3 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { updateElectronApp } from '..';
const repo = 'some-owner/some-repo';
import { autoUpdater, dialog } from 'electron';

jest.mock('electron');
import { updateElectronApp, makeUserNotifier, IUpdateInfo, IUpdateDialogStrings } from '../src';
const repo = 'some-owner/some-repo';

beforeEach(() => {
jest.useFakeTimers();
Expand Down Expand Up @@ -59,3 +59,61 @@ describe('updateElectronApp', () => {
});
});
});

describe('makeUserNotifier', () => {
const fakeUpdateInfo: IUpdateInfo = {
event: {} as Electron.Event,
releaseNotes: 'new release',
releaseName: 'v13.3.7',
releaseDate: new Date(),
updateURL: 'https://fake-update.url',
};

beforeEach(() => {
jest.mocked(dialog.showMessageBox).mockReset();
});

it('is a function that returns a callback function', () => {
expect(typeof makeUserNotifier).toBe('function');
expect(typeof makeUserNotifier()).toBe('function');
});

describe('callback', () => {
it.each([
['does', 0, 1],
['does not', 1, 0],
])('%s call autoUpdater.quitAndInstall if the user responds with %i', (_, response, called) => {
jest
.mocked(dialog.showMessageBox)
.mockResolvedValueOnce({ response, checkboxChecked: false });
const notifier = makeUserNotifier();
notifier(fakeUpdateInfo);

expect(dialog.showMessageBox).toHaveBeenCalled();
// quitAndInstall is only called after the showMessageBox promise resolves
process.nextTick(() => {
expect(autoUpdater.quitAndInstall).toHaveBeenCalledTimes(called);
});
});
});

it('can customize dialog properties', () => {
const strings: IUpdateDialogStrings = {
title: 'Custom Update Title',
detail: 'Custom update details',
restartButtonText: 'Custom restart string',
laterButtonText: 'Maybe not',
};

jest.mocked(dialog.showMessageBox).mockResolvedValue({ response: 0, checkboxChecked: false });
const notifier = makeUserNotifier(strings);
notifier(fakeUpdateInfo);
expect(dialog.showMessageBox).toHaveBeenCalledWith(
expect.objectContaining({
buttons: [strings.restartButtonText, strings.laterButtonText],
title: strings.title,
detail: strings.detail,
}),
);
});
});
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2032,6 +2032,11 @@ globals@^14.0.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==

globals@^15.13.0:
version "15.13.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-15.13.0.tgz#bbec719d69aafef188ecd67954aae76a696010fc"
integrity sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==

globalthis@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf"
Expand Down

0 comments on commit 691ad17

Please sign in to comment.