diff --git a/packages/electron-chrome-web-store/README.md b/packages/electron-chrome-web-store/README.md index 217fceb1..8169ffd4 100644 --- a/packages/electron-chrome-web-store/README.md +++ b/packages/electron-chrome-web-store/README.md @@ -100,6 +100,9 @@ Installs Chrome Web Store support in the specified session. - `allowlist`: An array of allowed extension IDs to install. - `denylist`: An array of denied extension IDs to install. - `beforeInstall`: A function which receives install details and returns a promise. Allows for prompting prior to install. + - `afterInstall`: A function which receives install details. Allows for additional actions after install. + - `afterUninstall`: A function which receives extension ID. Allows for additional actions after uninstall. + - `overrideExtensionInstallStatus`: A function which receives the current state, extension ID, and manifest. Returns a string indicating the install status of the extension, or returns undefined to fallback to the default install status. ### `installExtension` diff --git a/packages/electron-chrome-web-store/src/browser/api.ts b/packages/electron-chrome-web-store/src/browser/api.ts index 7515e4d6..3eccf00f 100644 --- a/packages/electron-chrome-web-store/src/browser/api.ts +++ b/packages/electron-chrome-web-store/src/browser/api.ts @@ -11,7 +11,7 @@ import { WebGlStatus, } from '../common/constants' import { installExtension, uninstallExtension } from './installer' -import { ExtensionId, WebStoreState } from './types' +import { ExtensionId, ExtensionStatusDetails, WebStoreState } from './types' const d = debug('electron-chrome-web-store:api') @@ -50,6 +50,23 @@ function getExtensionInstallStatus( extensionId: ExtensionId, manifest?: chrome.runtime.Manifest, ) { + if (state.overrideExtensionInstallStatus) { + const details: ExtensionStatusDetails = { + session: state.session, + extensionsPath: state.extensionsPath, + } + + const customStatus: unknown = state.overrideExtensionInstallStatus?.( + extensionId, + details, + manifest, + ) + + if (typeof customStatus === 'string') { + return customStatus + } + } + if (manifest && manifest.manifest_version < state.minimumManifestVersion) { return ExtensionInstallStatus.DEPRECATED_MANIFEST_VERSION } @@ -155,6 +172,19 @@ async function beginInstall( state.installing.add(extensionId) await installExtension(extensionId, state) + + if (state.afterInstall) { + // Doesn't need to await, just a callback + state.afterInstall({ + id: extensionId, + localizedName: details.localizedName, + manifest, + icon, + frame: senderFrame, + browserWindow: browserWindow || undefined, + }) + } + return { result: Result.SUCCESS } } catch (error) { console.error('Extension installation failed:', error) @@ -298,6 +328,13 @@ export function registerWebStoreApi(webStoreState: WebStoreState) { handle('chrome.management.setEnabled', async (event, id, enabled) => { // TODO: Implement enabling/disabling extension + if (webStoreState.customSetExtensionEnabled) { + const details: ExtensionStatusDetails = { + session: webStoreState.session, + extensionsPath: webStoreState.extensionsPath, + } + await webStoreState.customSetExtensionEnabled(id, details, enabled) + } return true }) @@ -310,9 +347,17 @@ export function registerWebStoreApi(webStoreState: WebStoreState) { try { await uninstallExtension(id, webStoreState) + queueMicrotask(() => { event.sender.send('chrome.management.onUninstalled', id) }) + + if (webStoreState.afterUninstall) { + queueMicrotask(() => { + webStoreState.afterUninstall?.({ id }) + }) + } + return Result.SUCCESS } catch (error) { console.error(error) diff --git a/packages/electron-chrome-web-store/src/browser/index.ts b/packages/electron-chrome-web-store/src/browser/index.ts index dcc3075a..3bfbd67c 100644 --- a/packages/electron-chrome-web-store/src/browser/index.ts +++ b/packages/electron-chrome-web-store/src/browser/index.ts @@ -10,7 +10,17 @@ export { installExtension, uninstallExtension, downloadExtension } from './insta import { initUpdater } from './updater' export { updateExtensions } from './updater' import { getDefaultExtensionsPath } from './utils' -import { BeforeInstall, ExtensionId, WebStoreState } from './types' +import { + BeforeInstall, + AfterInstall, + ExtensionId, + WebStoreState, + OverrideExtensionInstallStatus, + AfterUninstall, + CustomSetExtensionEnabled, +} from './types' +import { ExtensionInstallStatus } from '../common/constants' +export { ExtensionInstallStatus } function resolvePreloadPath(modulePath?: string) { // Attempt to resolve preload path from module exports @@ -97,6 +107,26 @@ interface ElectronChromeWebStoreOptions { * to be taken. */ beforeInstall?: BeforeInstall + + /** + * Called when setting the enabled status of an extension. + */ + customSetExtensionEnabled?: CustomSetExtensionEnabled + + /** + * Called when determining the install status of an extension. + */ + overrideExtensionInstallStatus?: OverrideExtensionInstallStatus + + /** + * Called after an extension is installed. + */ + afterInstall?: AfterInstall + + /** + * Called after an extension is uninstalled. + */ + afterUninstall?: AfterUninstall } /** @@ -113,7 +143,20 @@ export async function installChromeWebStore(opts: ElectronChromeWebStoreOptions const autoUpdate = typeof opts.autoUpdate === 'boolean' ? opts.autoUpdate : true const minimumManifestVersion = typeof opts.minimumManifestVersion === 'number' ? opts.minimumManifestVersion : 3 + const beforeInstall = typeof opts.beforeInstall === 'function' ? opts.beforeInstall : undefined + const afterInstall = typeof opts.afterInstall === 'function' ? opts.afterInstall : undefined + const afterUninstall = typeof opts.afterUninstall === 'function' ? opts.afterUninstall : undefined + + const customSetExtensionEnabled = + typeof opts.customSetExtensionEnabled === 'function' + ? opts.customSetExtensionEnabled + : undefined + + const overrideExtensionInstallStatus = + typeof opts.overrideExtensionInstallStatus === 'function' + ? opts.overrideExtensionInstallStatus + : undefined const webStoreState: WebStoreState = { session, @@ -123,6 +166,10 @@ export async function installChromeWebStore(opts: ElectronChromeWebStoreOptions denylist: opts.denylist ? new Set(opts.denylist) : undefined, minimumManifestVersion, beforeInstall, + afterInstall, + afterUninstall, + customSetExtensionEnabled, + overrideExtensionInstallStatus, } // Add preload script to session diff --git a/packages/electron-chrome-web-store/src/browser/installer.ts b/packages/electron-chrome-web-store/src/browser/installer.ts index 4431e4d0..5dd64693 100644 --- a/packages/electron-chrome-web-store/src/browser/installer.ts +++ b/packages/electron-chrome-web-store/src/browser/installer.ts @@ -13,7 +13,7 @@ import { readCrxFileHeader, readSignedData } from './crx3' import { convertHexadecimalToIDAlphabet, generateId } from './id' import { fetch, getChromeVersion, getDefaultExtensionsPath } from './utils' import { findExtensionInstall } from './loader' -import { ExtensionId } from './types' +import { AfterUninstall, ExtensionId } from './types' const d = debug('electron-chrome-web-store:installer') @@ -203,8 +203,6 @@ interface InstallExtensionOptions extends CommonExtensionOptions { loadExtensionOptions?: Electron.LoadExtensionOptions } -interface UninstallExtensionOptions extends CommonExtensionOptions {} - /** * Install extension from the web store. */ @@ -242,10 +240,7 @@ export async function installExtension( /** * Uninstall extension from the web store. */ -export async function uninstallExtension( - extensionId: string, - opts: UninstallExtensionOptions = {}, -) { +export async function uninstallExtension(extensionId: string, opts: CommonExtensionOptions = {}) { d('uninstalling %s', extensionId) const session = opts.session || electronSession.defaultSession diff --git a/packages/electron-chrome-web-store/src/browser/types.ts b/packages/electron-chrome-web-store/src/browser/types.ts index 2667fbbd..74794245 100644 --- a/packages/electron-chrome-web-store/src/browser/types.ts +++ b/packages/electron-chrome-web-store/src/browser/types.ts @@ -13,6 +13,27 @@ export type BeforeInstall = ( details: ExtensionInstallDetails, ) => Promise<{ action: 'allow' | 'deny' }> +export type AfterInstall = (details: ExtensionInstallDetails) => Promise + +export type AfterUninstall = (details: { id: ExtensionId }) => Promise + +export type ExtensionStatusDetails = { + session: Electron.Session + extensionsPath: string +} + +export type CustomSetExtensionEnabled = ( + extensionId: ExtensionId, + details: ExtensionStatusDetails, + enabled: boolean, +) => Promise + +export type OverrideExtensionInstallStatus = ( + extensionId: ExtensionId, + details: ExtensionStatusDetails, + manifest?: chrome.runtime.Manifest, +) => string | undefined + export interface WebStoreState { session: Electron.Session extensionsPath: string @@ -21,4 +42,8 @@ export interface WebStoreState { denylist?: Set minimumManifestVersion: number beforeInstall?: BeforeInstall + afterInstall?: AfterInstall + afterUninstall?: AfterUninstall + customSetExtensionEnabled?: CustomSetExtensionEnabled + overrideExtensionInstallStatus?: OverrideExtensionInstallStatus }