diff --git a/src/extension/constants/Keys.ts b/src/extension/constants/Keys.ts index 861ca09a..b6e523a1 100644 --- a/src/extension/constants/Keys.ts +++ b/src/extension/constants/Keys.ts @@ -8,3 +8,4 @@ export const SETTINGS_ADVANCED_KEY: string = 'settings_advanced'; export const SETTINGS_APPEARANCE_KEY: string = 'settings_appearance'; export const SETTINGS_GENERAL_KEY: string = 'settings_general'; export const SESSION_ITEM_KEY_PREFIX: string = 'session_'; +export const APP_WINDOW_KEY_PREFIX: string = 'app_window_'; diff --git a/src/extension/enums/AppTypeEnum.ts b/src/extension/enums/AppTypeEnum.ts new file mode 100644 index 00000000..9f50b0d4 --- /dev/null +++ b/src/extension/enums/AppTypeEnum.ts @@ -0,0 +1,7 @@ +enum AppTypeEnum { + BackgroundApp = 'background_app', + MainApp = 'main_app', + RegistrationApp = 'registration_app', +} + +export default AppTypeEnum; diff --git a/src/extension/enums/index.ts b/src/extension/enums/index.ts index 4831c101..6ba2355d 100644 --- a/src/extension/enums/index.ts +++ b/src/extension/enums/index.ts @@ -1,4 +1,5 @@ export { default as AccountsThunkEnum } from './AccountsThunkEnum'; +export { default as AppTypeEnum } from './AppTypeEnum'; export { default as AssetsThunkEnum } from './AssetsThunkEnum'; export { default as ErrorCodeEnum } from './ErrorCodeEnum'; export { default as MessagesThunkEnum } from './MessagesThunkEnum'; diff --git a/src/extension/services/AccountService.ts b/src/extension/services/AccountService.ts index e27efcbe..5dc2bcd7 100644 --- a/src/extension/services/AccountService.ts +++ b/src/extension/services/AccountService.ts @@ -39,7 +39,7 @@ export default class AccountService { } /** - * Public static functions + * public static functions */ /** @@ -152,7 +152,7 @@ export default class AccountService { } /** - * Private functions + * private functions */ /** @@ -165,7 +165,7 @@ export default class AccountService { } /** - * Public functions + * public functions */ /** diff --git a/src/extension/services/AppWindowManagerService.ts b/src/extension/services/AppWindowManagerService.ts new file mode 100644 index 00000000..d826f6c0 --- /dev/null +++ b/src/extension/services/AppWindowManagerService.ts @@ -0,0 +1,101 @@ +import { Windows } from 'webextension-polyfill'; + +// constants +import { APP_WINDOW_KEY_PREFIX } from '@extension/constants'; + +// enums +import { AppTypeEnum } from '@extension/enums'; + +// services +import StorageManager from './StorageManager'; + +// types +import { IBaseOptions, ILogger } from '@common/types'; +import { IAppWindow } from '@extension/types'; + +/** + * Manages app windows in storage. + * @class + */ +export default class AppWindowManagerService { + // private variables + private readonly logger: ILogger | null; + private readonly storageManager: StorageManager; + + constructor({ logger }: IBaseOptions) { + this.logger = logger || null; + this.storageManager = new StorageManager(); + } + + /** + * private functions + */ + + /** + * Convenience function that simply creates the app window item key from the window ID. + * @param {number} id - the window ID. + * @returns {string} the app window item key. + */ + private createAppWindowItemKey(id: number): string { + return `${APP_WINDOW_KEY_PREFIX}${id}`; + } + + /** + * public functions + */ + + public async getAll(): Promise { + const items: Record = + await this.storageManager.getAllItems(); + + return Object.keys(items).reduce( + (acc, key) => + key.startsWith(APP_WINDOW_KEY_PREFIX) + ? [...acc, items[key] as IAppWindow] + : acc, + [] + ); + } + + public async getById(id: number): Promise { + return await this.storageManager.getItem( + this.createAppWindowItemKey(id) + ); + } + + public async getByType(type: AppTypeEnum): Promise { + const appWindows: IAppWindow[] = await this.getAll(); + + return appWindows.filter((value) => value.type === type); + } + + public async removeById(id: number): Promise { + return await this.storageManager.remove(this.createAppWindowItemKey(id)); + } + + public async removeByType(type: AppTypeEnum): Promise { + const appWindows: IAppWindow[] = await this.getByType(type); + + return await this.storageManager.remove( + appWindows.map((value) => this.createAppWindowItemKey(value.windowId)) + ); + } + + public async saveByBrowserWindowAndType( + window: Windows.Window, + type: AppTypeEnum + ): Promise { + if (!window.id) { + return; + } + + return await this.storageManager.setItems({ + [this.createAppWindowItemKey(window.id)]: { + left: window.left || 0, + top: window.top || 0, + type, + windowId: window.id, + }, + }); + } +} diff --git a/src/extension/services/BackgroundService.ts b/src/extension/services/BackgroundService.ts index a2e894e7..d7a77db4 100644 --- a/src/extension/services/BackgroundService.ts +++ b/src/extension/services/BackgroundService.ts @@ -8,22 +8,25 @@ import { // enums import { EventNameEnum } from '@common/enums'; +import { AppTypeEnum } from '@extension/enums'; // events import { BaseEvent, ExtensionBackgroundAppLoadEvent } from '@common/events'; // services +import AppWindowManagerService from './AppWindowManagerService'; import PrivateKeyService from './PrivateKeyService'; import StorageManager from './StorageManager'; // types import { - IBaseOptions, IAllExtensionEvents, + IBaseOptions, IExtensionRequestEvents, IExtensionResponseEvents, ILogger, } from '@common/types'; +import { IAppWindow } from '@extension/types'; interface IBackgroundEvent { message: BaseEvent; @@ -31,22 +34,22 @@ interface IBackgroundEvent { } export default class BackgroundService { + private readonly appWindowManagerService: AppWindowManagerService; private backgroundEvents: IBackgroundEvent[]; private readonly logger: ILogger | null; - private mainWindow: Windows.Window | null; private readonly privateKeyService: PrivateKeyService; - private registrationWindow: Windows.Window | null; private readonly storageManager: StorageManager; constructor({ logger }: IBaseOptions) { + this.appWindowManagerService = new AppWindowManagerService({ + logger, + }); this.backgroundEvents = []; this.logger = logger || null; - this.mainWindow = null; this.privateKeyService = new PrivateKeyService({ logger, passwordTag: browser.runtime.id, }); - this.registrationWindow = null; this.storageManager = new StorageManager(); } @@ -88,13 +91,15 @@ export default class BackgroundService { ): Promise { const { id, event } = message; const isInitialized: boolean = await this.privateKeyService.isInitialized(); + const mainAppWindows: IAppWindow[] = + await this.appWindowManagerService.getByType(AppTypeEnum.MainApp); let backgroundWindow: Windows.Window; let searchParams: URLSearchParams; if ( !isInitialized || // not initialized, ignore it !sender.tab?.id || // no origin tab, no way to send a response, ignore - this.mainWindow // if the main window is open, let it handle the request + mainAppWindows.length > 0 // if a main window is open, let it handle the request ) { return; } @@ -144,40 +149,65 @@ export default class BackgroundService { } private async handleRegistrationCompleted(): Promise { + const _functionName: string = 'handleRegistrationCompleted'; + const mainAppWindows: IAppWindow[] = + await this.appWindowManagerService.getByType(AppTypeEnum.MainApp); + const registrationAppWindows: IAppWindow[] = + await this.appWindowManagerService.getByType(AppTypeEnum.RegistrationApp); + let mainWindow: Windows.Window; + this.logger && this.logger.debug( - `${BackgroundService.name}#handleRegistrationCompleted(): extension message "${EventNameEnum.ExtensionRegistrationCompleted}" received from the popup` + `${BackgroundService.name}#${_functionName}(): extension message "${EventNameEnum.ExtensionRegistrationCompleted}" received from the popup` ); - // if there is no main window, create a new one - if (!this.mainWindow) { - this.mainWindow = await browser.windows.create({ + // if there is no main app windows, create a new one + if (mainAppWindows.length <= 0) { + mainWindow = await browser.windows.create({ height: DEFAULT_POPUP_HEIGHT, type: 'popup', url: 'main-app.html', width: DEFAULT_POPUP_WIDTH, - ...(this.registrationWindow && { - left: this.registrationWindow.left, - top: this.registrationWindow.top, + ...(registrationAppWindows[0] && { + left: registrationAppWindows[0].left, + top: registrationAppWindows[0].top, }), }); + + // save to storage + await this.appWindowManagerService.saveByBrowserWindowAndType( + mainWindow, + AppTypeEnum.MainApp + ); } - // if the register window exists remove it - if (this.registrationWindow && this.registrationWindow.id) { - await browser.windows.remove(this.registrationWindow.id); + // if registration app windows exist remove them + if (registrationAppWindows.length > 0) { + await Promise.all( + registrationAppWindows.map( + async (value) => await browser.windows.remove(value.windowId) + ) + ); } } private async handleReset(): Promise { + const _functionName: string = 'handleReset'; + const mainAppWindows: IAppWindow[] = + await this.appWindowManagerService.getByType(AppTypeEnum.MainApp); + this.logger && this.logger.debug( - `${BackgroundService.name}#handleReset(): extension message "${EventNameEnum.ExtensionReset}" received from the popup` + `${BackgroundService.name}#${_functionName}(): extension message "${EventNameEnum.ExtensionReset}" received from the popup` ); - // remove the main window if it exists - if (this.mainWindow && this.mainWindow.id) { - await browser.windows.remove(this.mainWindow.id); + // remove the main app windows if they exist + if (mainAppWindows.length > 0) { + await Promise.all( + mainAppWindows.map( + async (value) => await browser.windows.remove(value.windowId) + ) + ); } // remove any background windows @@ -242,49 +272,64 @@ export default class BackgroundService { } public async onExtensionClick(): Promise { + const _functionName: string = 'onExtensionClick'; const isInitialized: boolean = await this.privateKeyService.isInitialized(); + const mainAppWindows: IAppWindow[] = + await this.appWindowManagerService.getByType(AppTypeEnum.MainApp); + let mainWindow: Windows.Window; + let registrationWindow: Windows.Window; if (!isInitialized) { this.logger && this.logger.debug( - `${BackgroundService.name}#onExtensionClick(): no account detected, registering new account` + `${BackgroundService.name}#${_functionName}(): no account detected, registering new account` ); // remove everything from storage await this.storageManager.removeAll(); - this.registrationWindow = await browser.windows.create({ + registrationWindow = await browser.windows.create({ height: DEFAULT_POPUP_HEIGHT, type: 'popup', url: 'registration-app.html', width: DEFAULT_POPUP_WIDTH, }); - return; + // save the registration window to storage + return await this.appWindowManagerService.saveByBrowserWindowAndType( + registrationWindow, + AppTypeEnum.RegistrationApp + ); } - // if there is no main window up, we can open the app - if (!this.mainWindow) { + // if there is no main app window up, we can open the app + if (mainAppWindows.length <= 0) { this.logger && this.logger.debug( - `${BackgroundService.name}#onExtensionClick(): previous account detected, opening main app` + `${BackgroundService.name}#${_functionName}(): previous account detected, opening main app` ); - this.mainWindow = await browser.windows.create({ + mainWindow = await browser.windows.create({ height: DEFAULT_POPUP_HEIGHT, type: 'popup', url: 'main-app.html', width: DEFAULT_POPUP_WIDTH, }); - return; + // save the main app window to storage + return await this.appWindowManagerService.saveByBrowserWindowAndType( + mainWindow, + AppTypeEnum.MainApp + ); } } - public onWindowRemove(windowId: number): void { + public async onWindowRemove(windowId: number): Promise { + const _functionName: string = 'onWindowRemove'; const backgroundEvent: IBackgroundEvent | null = this.backgroundEvents.find((value) => value.windowId === windowId) || null; + let appWindow: IAppWindow | null; if (backgroundEvent) { this.logger && @@ -298,22 +343,16 @@ export default class BackgroundService { ); } - if (this.mainWindow && this.mainWindow.id === windowId) { - this.logger && - this.logger.debug( - `${BackgroundService.name}#onWindowRemove(): removed main app window` - ); - - this.mainWindow = null; - } + appWindow = await this.appWindowManagerService.getById(windowId); - if (this.registrationWindow && this.registrationWindow.id === windowId) { + // remove the app window from storage + if (appWindow) { this.logger && this.logger.debug( - `${BackgroundService.name}#onWindowRemove(): removed registration app window` + `${BackgroundService.name}#${_functionName}(): removed "${appWindow.type}" window` ); - this.registrationWindow = null; + await this.appWindowManagerService.removeById(windowId); } } } diff --git a/src/extension/services/index.ts b/src/extension/services/index.ts index 322f1c7e..ec500ecb 100644 --- a/src/extension/services/index.ts +++ b/src/extension/services/index.ts @@ -1,4 +1,5 @@ export { default as AccountService } from './AccountService'; +export { default as AppWindowManagerService } from './AppWindowManagerService'; export { default as BackgroundService } from './BackgroundService'; export { default as ColorModeManager } from './ColorModeManager'; export { default as ExternalEventService } from './ExternalEventService'; diff --git a/src/extension/types/IAppWindow.ts b/src/extension/types/IAppWindow.ts new file mode 100644 index 00000000..f920b208 --- /dev/null +++ b/src/extension/types/IAppWindow.ts @@ -0,0 +1,11 @@ +// enums +import { AppTypeEnum } from '@extension/enums'; + +interface IAppWindow { + left: number; + top: number; + type: AppTypeEnum; + windowId: number; +} + +export default IAppWindow; diff --git a/src/extension/types/IStorageItemTypes.ts b/src/extension/types/IStorageItemTypes.ts index 3dc86508..3b7bd6b4 100644 --- a/src/extension/types/IStorageItemTypes.ts +++ b/src/extension/types/IStorageItemTypes.ts @@ -2,6 +2,7 @@ import IAccount from './IAccount'; import IAdvancedSettings from './IAdvancedSettings'; import IAppearanceSettings from './IAppearanceSettings'; +import IAppWindow from './IAppWindow'; import IAsset from './IAsset'; import IGeneralSettings from './IGeneralSettings'; import IPasswordTag from './IPasswordTag'; @@ -13,6 +14,7 @@ type IStorageItemTypes = | IAccount | IAdvancedSettings | IAppearanceSettings + | IAppWindow | IAsset[] | IGeneralSettings | IPasswordTag diff --git a/src/extension/types/index.ts b/src/extension/types/index.ts index 2bdaa6ef..f2b20b1f 100644 --- a/src/extension/types/index.ts +++ b/src/extension/types/index.ts @@ -26,6 +26,7 @@ export type { default as IAppProps } from './IAppProps'; export type { default as IAppThunkDispatch } from './IAppThunkDispatch'; export type { default as IApplicationTransaction } from './IApplicationTransaction'; export type { default as IApplicationTransactionTypes } from './IApplicationTransactionTypes'; +export type { default as IAppWindow } from './IAppWindow'; export type { default as IAsset } from './IAsset'; export type { default as IAssetConfigTransaction } from './IAssetConfigTransaction'; export type { default as IAssetCreateTransaction } from './IAssetCreateTransaction';