diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index c3722f2a480..41f300270eb 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -230,6 +230,8 @@ import { BrowserPlatformUtilsService } from "../platform/services/platform-utils import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; +import { ForegroundSyncService } from "../platform/sync/foreground-sync.service"; +import { SyncServiceListener } from "../platform/sync/sync-service.listener"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; @@ -339,6 +341,7 @@ export default class MainBackground { scriptInjectorService: BrowserScriptInjectorService; kdfConfigService: kdfConfigServiceAbstraction; offscreenDocumentService: OffscreenDocumentService; + syncServiceListener: SyncServiceListener; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -377,7 +380,8 @@ export default class MainBackground { const logoutCallback = async (expired: boolean, userId?: UserId) => await this.logout(expired, userId); - this.logService = new ConsoleLogService(false); + const isDev = process.env.ENV === "development"; + this.logService = new ConsoleLogService(isDev); this.cryptoFunctionService = new WebCryptoFunctionService(self); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.storageService = new BrowserLocalStorageService(); @@ -396,7 +400,7 @@ export default class MainBackground { ), ); - this.offscreenDocumentService = new DefaultOffscreenDocumentService(); + this.offscreenDocumentService = new DefaultOffscreenDocumentService(this.logService); this.platformUtilsService = new BackgroundPlatformUtilsService( this.messagingService, @@ -792,32 +796,52 @@ export default class MainBackground { this.providerService = new ProviderService(this.stateProvider); - this.syncService = new SyncService( - this.masterPasswordService, - this.accountService, - this.apiService, - this.domainSettingsService, - this.folderService, - this.cipherService, - this.cryptoService, - this.collectionService, - this.messagingService, - this.policyService, - this.sendService, - this.logService, - this.keyConnectorService, - this.stateService, - this.providerService, - this.folderApiService, - this.organizationService, - this.sendApiService, - this.userDecryptionOptionsService, - this.avatarService, - logoutCallback, - this.billingAccountProfileStateService, - this.tokenService, - this.authService, - ); + if (this.popupOnlyContext) { + this.syncService = new ForegroundSyncService( + this.stateService, + this.folderService, + this.folderApiService, + this.messagingService, + this.logService, + this.cipherService, + this.collectionService, + this.apiService, + this.accountService, + this.authService, + this.sendService, + this.sendApiService, + messageListener, + ); + } else { + this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, + this.apiService, + this.domainSettingsService, + this.folderService, + this.cipherService, + this.cryptoService, + this.collectionService, + this.messagingService, + this.policyService, + this.sendService, + this.logService, + this.keyConnectorService, + this.stateService, + this.providerService, + this.folderApiService, + this.organizationService, + this.sendApiService, + this.userDecryptionOptionsService, + this.avatarService, + logoutCallback, + this.billingAccountProfileStateService, + this.tokenService, + this.authService, + ); + + this.syncServiceListener = new SyncServiceListener(this.syncService, messageListener); + } this.eventUploadService = new EventUploadService( this.apiService, this.stateProvider, @@ -1141,6 +1165,7 @@ export default class MainBackground { this.contextMenusBackground?.init(); await this.idleBackground.init(); this.webRequestBackground?.startListening(); + this.syncServiceListener?.startListening(); return new Promise((resolve) => { setTimeout(async () => { diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts index d6be0a924e5..c9bdd823a52 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts @@ -1,3 +1,7 @@ +import { mock } from "jest-mock-extended"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + import { DefaultOffscreenDocumentService } from "./offscreen-document.service"; class TestCase { @@ -21,6 +25,7 @@ describe.each([ new TestCase("synchronous callback", () => 42), new TestCase("asynchronous callback", () => Promise.resolve(42)), ])("DefaultOffscreenDocumentService %s", (testCase) => { + const logService = mock(); let sut: DefaultOffscreenDocumentService; const reasons = [chrome.offscreen.Reason.TESTING]; const justification = "justification is testing"; @@ -37,7 +42,7 @@ describe.each([ callback = testCase.callback; chrome.offscreen = api; - sut = new DefaultOffscreenDocumentService(); + sut = new DefaultOffscreenDocumentService(logService); }); afterEach(() => { diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts index da0ca382698..a260e3ca6c8 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts @@ -1,7 +1,9 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + export class DefaultOffscreenDocumentService implements DefaultOffscreenDocumentService { private workerCount = 0; - constructor() {} + constructor(private logService: LogService) {} async withDocument( reasons: chrome.offscreen.Reason[], @@ -24,11 +26,21 @@ export class DefaultOffscreenDocumentService implements DefaultOffscreenDocument } private async create(reasons: chrome.offscreen.Reason[], justification: string): Promise { - await chrome.offscreen.createDocument({ - url: "offscreen-document/index.html", - reasons, - justification, - }); + try { + await chrome.offscreen.createDocument({ + url: "offscreen-document/index.html", + reasons, + justification, + }); + } catch (e) { + // gobble multiple offscreen document creation errors + // TODO: remove this when the offscreen document service is fixed PM-8014 + if (e.message === "Only a single offscreen document may be created.") { + this.logService.info("Ignoring offscreen document creation error."); + return; + } + throw e; + } } private async close(): Promise { diff --git a/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.html b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.html new file mode 100644 index 00000000000..6cc7e317e27 --- /dev/null +++ b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.html @@ -0,0 +1,11 @@ +
+
+

+ {{ title }} +

+ +
+
+ +
+
diff --git a/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.ts b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.ts new file mode 100644 index 00000000000..b33a2a0f330 --- /dev/null +++ b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from "@angular/core"; + +import { TypographyModule } from "@bitwarden/components"; + +@Component({ + standalone: true, + selector: "popup-section-header", + templateUrl: "./popup-section-header.component.html", + imports: [TypographyModule], +}) +export class PopupSectionHeaderComponent { + @Input() title: string; +} diff --git a/apps/browser/src/platform/popup/popup-section-header/popup-section-header.stories.ts b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.stories.ts new file mode 100644 index 00000000000..450bfb24226 --- /dev/null +++ b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.stories.ts @@ -0,0 +1,90 @@ +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; + +import { + CardComponent, + IconButtonModule, + SectionComponent, + TypographyModule, +} from "@bitwarden/components"; + +import { PopupSectionHeaderComponent } from "./popup-section-header.component"; + +export default { + title: "Browser/Popup Section Header", + component: PopupSectionHeaderComponent, + args: { + title: "Title", + }, + decorators: [ + moduleMetadata({ + imports: [SectionComponent, CardComponent, TypographyModule, IconButtonModule], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const OnlyTitle: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + args: { + title: "Only Title", + }, +}; + +export const TrailingText: Story = { + render: (args) => ({ + props: args, + template: ` + + 13 + + `, + }), + args: { + title: "Trailing Text", + }, +}; + +export const TailingIcon: Story = { + render: (args) => ({ + props: args, + template: ` + + + + `, + }), + args: { + title: "Trailing Icon", + }, +}; + +export const WithSections: Story = { + render: () => ({ + template: ` +
+ + + + + +

Card 1 Content

+
+
+ + + + + +

Card 2 Content

+
+
+
+ `, + }), +}; diff --git a/apps/browser/src/platform/sync/foreground-sync.service.ts b/apps/browser/src/platform/sync/foreground-sync.service.ts new file mode 100644 index 00000000000..3c144316724 --- /dev/null +++ b/apps/browser/src/platform/sync/foreground-sync.service.ts @@ -0,0 +1,79 @@ +import { firstValueFrom, timeout } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { + CommandDefinition, + MessageListener, + MessageSender, +} from "@bitwarden/common/platform/messaging"; +import { CoreSyncService } from "@bitwarden/common/platform/sync/internal"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; +import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; + +const SYNC_COMPLETED = new CommandDefinition<{ successfully: boolean }>("syncCompleted"); +export const DO_FULL_SYNC = new CommandDefinition<{ + forceSync: boolean; + allowThrowOnError: boolean; +}>("doFullSync"); + +export class ForegroundSyncService extends CoreSyncService { + constructor( + stateService: StateService, + folderService: InternalFolderService, + folderApiService: FolderApiServiceAbstraction, + messageSender: MessageSender, + logService: LogService, + cipherService: CipherService, + collectionService: CollectionService, + apiService: ApiService, + accountService: AccountService, + authService: AuthService, + sendService: InternalSendService, + sendApiService: SendApiService, + private readonly messageListener: MessageListener, + ) { + super( + stateService, + folderService, + folderApiService, + messageSender, + logService, + cipherService, + collectionService, + apiService, + accountService, + authService, + sendService, + sendApiService, + ); + } + + async fullSync(forceSync: boolean, allowThrowOnError: boolean = false): Promise { + this.syncInProgress = true; + try { + const syncCompletedPromise = firstValueFrom( + this.messageListener.messages$(SYNC_COMPLETED).pipe( + timeout({ + first: 10_000, + with: () => { + throw new Error("Timeout while doing a fullSync call."); + }, + }), + ), + ); + this.messageSender.send(DO_FULL_SYNC, { forceSync, allowThrowOnError }); + const result = await syncCompletedPromise; + return result.successfully; + } finally { + this.syncInProgress = false; + } + } +} diff --git a/apps/browser/src/platform/sync/sync-service.listener.ts b/apps/browser/src/platform/sync/sync-service.listener.ts new file mode 100644 index 00000000000..b9e18accacd --- /dev/null +++ b/apps/browser/src/platform/sync/sync-service.listener.ts @@ -0,0 +1,25 @@ +import { Subscription, concatMap, filter } from "rxjs"; + +import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; + +import { DO_FULL_SYNC } from "./foreground-sync.service"; + +export class SyncServiceListener { + constructor( + private readonly syncService: SyncService, + private readonly messageListener: MessageListener, + ) {} + + startListening(): Subscription { + return this.messageListener + .messages$(DO_FULL_SYNC) + .pipe( + filter((message) => isExternalMessage(message)), + concatMap(async ({ forceSync, allowThrowOnError }) => { + await this.syncService.fullSync(forceSync, allowThrowOnError); + }), + ) + .subscribe(); + } +} diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 05158d3295d..74e24433b2c 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -47,6 +47,7 @@ import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.comp import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../platform/popup/layout/popup-page.component"; import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component"; +import { PopupSectionHeaderComponent } from "../platform/popup/popup-section-header/popup-section-header.component"; import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component"; import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; @@ -124,6 +125,7 @@ import "../platform/popup/locales"; PopupFooterComponent, PopupHeaderComponent, UserVerificationDialogComponent, + PopupSectionHeaderComponent, ], declarations: [ ActionButtonsComponent, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 0b9c8f6fe68..32d4adae4a1 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -195,9 +195,11 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: LogService, - useFactory: (platformUtilsService: PlatformUtilsService) => - new ConsoleLogService(platformUtilsService.isDev()), - deps: [PlatformUtilsService], + useFactory: () => { + const isDev = process.env.ENV === "development"; + return new ConsoleLogService(isDev); + }, + deps: [], }), safeProvider({ provide: EnvironmentService, @@ -286,7 +288,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OffscreenDocumentService, useClass: DefaultOffscreenDocumentService, - deps: [], + deps: [LogService], }), safeProvider({ provide: PlatformUtilsService, diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 2b3be149749..d1a48a78e11 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -1,5 +1,9 @@ -