diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml new file mode 100644 index 0000000000..ea9e69226a --- /dev/null +++ b/.github/workflows/scan.yml @@ -0,0 +1,72 @@ +name: Scan + +on: + workflow_dispatch: + push: + branches: + - "main" + - "rc" + - "hotfix-rc" + pull_request_target: + types: [opened, synchronize] + +permissions: read-all + +jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + + sast: + name: SAST scan + runs-on: ubuntu-22.04 + needs: check-run + permissions: + security-events: write + + steps: + - name: Check out repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Scan with Checkmarx + uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23 + env: + INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" + with: + project_name: ${{ github.repository }} + cx_tenant: ${{ secrets.CHECKMARX_TENANT }} + base_uri: https://ast.checkmarx.net/ + cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }} + cx_client_secret: ${{ secrets.CHECKMARX_SECRET }} + additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }} + + - name: Upload Checkmarx results to GitHub + uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 + with: + sarif_file: cx_result.sarif + + quality: + name: Quality scan + runs-on: ubuntu-22.04 + needs: check-run + + steps: + - name: Check out repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Scan with SonarCloud + uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: > + -Dsonar.organization=${{ github.repository_owner }} + -Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }} + -Dsonar.test.inclusions=**/*.spec.ts + -Dsonar.tests=. diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 2bec7e9a24..f8454d4bd5 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -119,7 +119,7 @@ "message": "Дадаць лагін" }, "addCardMenu": { - "message": "Add card" + "message": "Дадаць картку" }, "addIdentityMenu": { "message": "Add identity" @@ -269,7 +269,7 @@ "message": "Даўжыня" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Мінімальная даўжыня пароля" }, "uppercase": { "message": "Вялікія літары (A-Z)" @@ -369,7 +369,7 @@ "message": "Iншае" }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Наладзіць метад разблакіроўкі для змянення дзеяння часу чакання вашага сховішча." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" @@ -415,7 +415,7 @@ "message": "Заблакіраваць зараз" }, "lockAll": { - "message": "Lock all" + "message": "Заблакаваць усе" }, "immediately": { "message": "Адразу" @@ -494,7 +494,7 @@ "message": "Ваш уліковы запіс створаны! Цяпер вы можаце ўвайсці ў яго." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Вы паспяхова аўтарызаваны" }, "youMayCloseThisWindow": { "message": "You may close this window" @@ -2005,7 +2005,7 @@ "message": "Выбраць папку..." }, "noFoldersFound": { - "message": "No folders found", + "message": "Папкі не знойдзены", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2215,7 +2215,7 @@ "message": "Версія сервера" }, "selfHostedServer": { - "message": "self-hosted" + "message": "уласнае размяшчэнне" }, "thirdParty": { "message": "Іншы пастаўшчык" @@ -2356,25 +2356,25 @@ } }, "loggingInOn": { - "message": "Logging in on" + "message": "Увайсці на" }, "opensInANewWindow": { "message": "Адкрываць у новым акне" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Патрабуецца ўхваленне прылады. Выберыце параметры ўхвалення ніжэй:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Запомніць гэту прыладу" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Здыміце пазнаку, калі выкарыстоўваеце агульнадаступную прыладу" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Ухваліць з іншай вашай прылады" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Запытаць ухваленне адміністратара" }, "approveWithMasterPassword": { "message": "Approve with master password" @@ -2402,7 +2402,7 @@ "message": "Адлюстраванне" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Уліковы запіс паспяхова створаны!" }, "adminApprovalRequested": { "message": "Admin approval requested" diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index f3277bdbc3..f58743f4ad 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -688,7 +688,7 @@ "message": "A bejelentkezési jelszó frissítésének kérése, ha változást lett érzékelve egy webhelyen. Minden bejelentkezett fiókra vonatkozik." }, "enableUsePasskeys": { - "message": "Kérés a jhozzáférési kulcs mentésére és használatára" + "message": "Kérés a hozzáférési kulcs mentésére és használatára" }, "usePasskeysDesc": { "message": "Kérés az új hozzáféréi kulcsok mentésére vagy bejelentkezés a széfben tárolt hozzáférési kulcsokkal. Minden bejelentkezett fiókra vonatkozik." diff --git a/apps/browser/src/admin-console/background/service-factories/organization-service.factory.ts b/apps/browser/src/admin-console/background/service-factories/organization-service.factory.ts index c77508b0f8..b7f6f98ea2 100644 --- a/apps/browser/src/admin-console/background/service-factories/organization-service.factory.ts +++ b/apps/browser/src/admin-console/background/service-factories/organization-service.factory.ts @@ -1,4 +1,5 @@ import { OrganizationService as AbstractOrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { FactoryOptions, @@ -6,11 +7,7 @@ import { factory, } from "../../../platform/background/service-factories/factory-options"; import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory"; -import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; -import { BrowserOrganizationService } from "../../services/browser-organization.service"; +import { StateServiceInitOptions } from "../../../platform/background/service-factories/state-service.factory"; type OrganizationServiceFactoryOptions = FactoryOptions; @@ -25,10 +22,6 @@ export function organizationServiceFactory( cache, "organizationService", opts, - async () => - new BrowserOrganizationService( - await stateServiceFactory(cache, opts), - await stateProviderFactory(cache, opts), - ), + async () => new OrganizationService(await stateProviderFactory(cache, opts)), ); } diff --git a/apps/browser/src/admin-console/services/browser-organization.service.ts b/apps/browser/src/admin-console/services/browser-organization.service.ts deleted file mode 100644 index 6294756cdf..0000000000 --- a/apps/browser/src/admin-console/services/browser-organization.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; - -import { browserSession, sessionSync } from "../../platform/decorators/session-sync-observable"; - -@browserSession -export class BrowserOrganizationService extends OrganizationService { - @sessionSync({ initializer: Organization.fromJSON, initializeAs: "array" }) - protected _organizations: BehaviorSubject; -} diff --git a/apps/browser/src/auth/background/service-factories/avatar-service.factory.ts b/apps/browser/src/auth/background/service-factories/avatar-service.factory.ts new file mode 100644 index 0000000000..456edfa93d --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/avatar-service.factory.ts @@ -0,0 +1,38 @@ +import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; +import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; + +import { + ApiServiceInitOptions, + apiServiceFactory, +} from "../../../platform/background/service-factories/api-service.factory"; +import { + CachedServices, + factory, + FactoryOptions, +} from "../../../platform/background/service-factories/factory-options"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type AvatarServiceFactoryOptions = FactoryOptions; + +export type AvatarServiceInitOptions = AvatarServiceFactoryOptions & + ApiServiceInitOptions & + StateProviderInitOptions; + +export function avatarServiceFactory( + cache: { avatarService?: AvatarServiceAbstraction } & CachedServices, + opts: AvatarServiceInitOptions, +): Promise { + return factory( + cache, + "avatarService", + opts, + async () => + new AvatarService( + await apiServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), + ), + ); +} diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index 078bfb8a63..b0ae87a75f 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -9,6 +9,7 @@ import { ApiServiceInitOptions, } from "../../../platform/background/service-factories/api-service.factory"; import { appIdServiceFactory } from "../../../platform/background/service-factories/app-id-service.factory"; +import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -119,6 +120,7 @@ export function loginStrategyServiceFactory( await deviceTrustCryptoServiceFactory(cache, opts), await authRequestServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), + await billingAccountProfileStateServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/token-service.factory.ts b/apps/browser/src/auth/background/service-factories/token-service.factory.ts index 476b8e2d78..25c30460f0 100644 --- a/apps/browser/src/auth/background/service-factories/token-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/token-service.factory.ts @@ -7,13 +7,29 @@ import { factory, } from "../../../platform/background/service-factories/factory-options"; import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; + GlobalStateProviderInitOptions, + globalStateProviderFactory, +} from "../../../platform/background/service-factories/global-state-provider.factory"; +import { + PlatformUtilsServiceInitOptions, + platformUtilsServiceFactory, +} from "../../../platform/background/service-factories/platform-utils-service.factory"; +import { + SingleUserStateProviderInitOptions, + singleUserStateProviderFactory, +} from "../../../platform/background/service-factories/single-user-state-provider.factory"; +import { + SecureStorageServiceInitOptions, + secureStorageServiceFactory, +} from "../../../platform/background/service-factories/storage-service.factory"; type TokenServiceFactoryOptions = FactoryOptions; -export type TokenServiceInitOptions = TokenServiceFactoryOptions & StateServiceInitOptions; +export type TokenServiceInitOptions = TokenServiceFactoryOptions & + SingleUserStateProviderInitOptions & + GlobalStateProviderInitOptions & + PlatformUtilsServiceInitOptions & + SecureStorageServiceInitOptions; export function tokenServiceFactory( cache: { tokenService?: AbstractTokenService } & CachedServices, @@ -23,6 +39,12 @@ export function tokenServiceFactory( cache, "tokenService", opts, - async () => new TokenService(await stateServiceFactory(cache, opts)), + async () => + new TokenService( + await singleUserStateProviderFactory(cache, opts), + await globalStateProviderFactory(cache, opts), + (await platformUtilsServiceFactory(cache, opts)).supportsSecureStorage(), + await secureStorageServiceFactory(cache, opts), + ), ); } diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.ts b/apps/browser/src/auth/popup/account-switching/current-account.component.ts index 06612b8d33..1c7f93bf30 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.ts @@ -1,32 +1,61 @@ import { Location } from "@angular/common"; import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import { Observable, combineLatest, switchMap } from "rxjs"; -import { CurrentAccountService } from "./services/current-account.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { UserId } from "@bitwarden/common/types/guid"; + +export type CurrentAccount = { + id: UserId; + name: string | undefined; + email: string; + status: AuthenticationStatus; + avatarColor: string; +}; @Component({ selector: "app-current-account", templateUrl: "current-account.component.html", }) export class CurrentAccountComponent { + currentAccount$: Observable; + constructor( - private currentAccountService: CurrentAccountService, + private accountService: AccountService, + private avatarService: AvatarService, private router: Router, private location: Location, private route: ActivatedRoute, - ) {} + ) { + this.currentAccount$ = combineLatest([ + this.accountService.activeAccount$, + this.avatarService.avatarColor$, + ]).pipe( + switchMap(async ([account, avatarColor]) => { + if (account == null) { + return null; + } + const currentAccount: CurrentAccount = { + id: account.id, + name: account.name || account.email, + email: account.email, + status: account.status, + avatarColor, + }; - get currentAccount$() { - return this.currentAccountService.currentAccount$; + return currentAccount; + }), + ); } async currentAccountClicked() { if (this.route.snapshot.data.state.includes("account-switcher")) { this.location.back(); } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/account-switcher"]); + await this.router.navigate(["/account-switcher"]); } } } diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts index 9845fac1da..f02a8ee201 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts @@ -1,12 +1,12 @@ import { matches, mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom, timeout } from "rxjs"; +import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { UserId } from "@bitwarden/common/types/guid"; import { AccountSwitcherService } from "./account-switcher.service"; @@ -16,7 +16,7 @@ describe("AccountSwitcherService", () => { const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null); const accountService = mock(); - const stateService = mock(); + const avatarService = mock(); const messagingService = mock(); const environmentService = mock(); const logService = mock(); @@ -25,11 +25,13 @@ describe("AccountSwitcherService", () => { beforeEach(() => { jest.resetAllMocks(); + accountService.accounts$ = accountsSubject; accountService.activeAccount$ = activeAccountSubject; + accountSwitcherService = new AccountSwitcherService( accountService, - stateService, + avatarService, messagingService, environmentService, logService, @@ -44,6 +46,7 @@ describe("AccountSwitcherService", () => { status: AuthenticationStatus.Unlocked, }; + avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc")); accountsSubject.next({ "1": user1AccountInfo, } as Record); @@ -72,6 +75,7 @@ describe("AccountSwitcherService", () => { status: AuthenticationStatus.Unlocked, }; } + avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc")); accountsSubject.next(seedAccounts); activeAccountSubject.next( Object.assign(seedAccounts["1" as UserId], { id: "1" as UserId }), diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts index 0b1015544c..cf78f2ff91 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts @@ -11,11 +11,11 @@ import { } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { UserId } from "@bitwarden/common/types/guid"; import { fromChromeEvent } from "../../../../platform/browser/from-chrome-event"; @@ -44,7 +44,7 @@ export class AccountSwitcherService { constructor( private accountService: AccountService, - private stateService: StateService, + private avatarService: AvatarService, private messagingService: MessagingService, private environmentService: EnvironmentService, private logService: LogService, @@ -68,7 +68,9 @@ export class AccountSwitcherService { server: await this.environmentService.getHost(id), status: account.status, isActive: id === activeAccount?.id, - avatarColor: await this.stateService.getAvatarColor({ userId: id }), + avatarColor: await firstValueFrom( + this.avatarService.getUserAvatarColor$(id as UserId), + ), }; }), ); diff --git a/apps/browser/src/auth/popup/account-switching/services/current-account.service.ts b/apps/browser/src/auth/popup/account-switching/services/current-account.service.ts deleted file mode 100644 index 21fc3bdac4..0000000000 --- a/apps/browser/src/auth/popup/account-switching/services/current-account.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from "@angular/core"; -import { Observable, switchMap } from "rxjs"; - -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { UserId } from "@bitwarden/common/types/guid"; - -export type CurrentAccount = { - id: UserId; - name: string | undefined; - email: string; - status: AuthenticationStatus; - avatarColor: string; -}; - -@Injectable({ - providedIn: "root", -}) -export class CurrentAccountService { - currentAccount$: Observable; - - constructor( - private accountService: AccountService, - private stateService: StateService, - ) { - this.currentAccount$ = this.accountService.activeAccount$.pipe( - switchMap(async (account) => { - if (account == null) { - return null; - } - const currentAccount: CurrentAccount = { - id: account.id, - name: account.name || account.email, - email: account.email, - status: account.status, - avatarColor: await this.stateService.getAvatarColor({ userId: account.id }), - }; - - return currentAccount; - }), - ); - } -} diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 857dae6630..c1dd952658 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -91,6 +91,8 @@ export class LoginComponent extends BaseLoginComponent { } async launchSsoBrowser() { + // Save off email for SSO + await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); await this.loginService.saveEmailSettings(); // Generate necessary sso params const passwordOptions: any = { diff --git a/apps/browser/src/auth/popup/two-factor-options.component.ts b/apps/browser/src/auth/popup/two-factor-options.component.ts index d3d7b7f33a..bad2e4a9e7 100644 --- a/apps/browser/src/auth/popup/two-factor-options.component.ts +++ b/apps/browser/src/auth/popup/two-factor-options.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -16,9 +17,10 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, private activatedRoute: ActivatedRoute, ) { - super(twoFactorService, router, i18nService, platformUtilsService, window); + super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService); } close() { diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 48c3ce8bbe..d7156f4272 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -8,11 +8,21 @@ import { AutofillOverlayVisibility, } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { + DefaultDomainSettingsService, + DomainSettingsService, +} from "@bitwarden/common/autofill/services/domain-settings.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { SettingsService } from "@bitwarden/common/services/settings.service"; +import { + FakeStateProvider, + FakeAccountService, + mockAccountServiceWith, +} from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -41,6 +51,10 @@ import OverlayBackground from "./overlay.background"; const iconServerUrl = "https://icons.bitwarden.com/"; describe("OverlayBackground", () => { + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); + let domainSettingsService: DomainSettingsService; let buttonPortSpy: chrome.runtime.Port; let listPortSpy: chrome.runtime.Port; let overlayBackground: OverlayBackground; @@ -50,7 +64,6 @@ describe("OverlayBackground", () => { const environmentService = mock({ getIconsUrl: () => iconServerUrl, }); - const settingsService = mock(); const stateService = mock(); const autofillSettingsService = mock(); const i18nService = mock(); @@ -72,12 +85,13 @@ describe("OverlayBackground", () => { }; beforeEach(() => { + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); overlayBackground = new OverlayBackground( cipherService, autofillService, authService, environmentService, - settingsService, + domainSettingsService, stateService, autofillSettingsService, i18nService, @@ -90,6 +104,7 @@ describe("OverlayBackground", () => { .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); themeStateService.selectedTheme$ = of(ThemeType.Light); + domainSettingsService.showFavicons$ = of(true); void overlayBackground.init(); }); @@ -274,7 +289,7 @@ describe("OverlayBackground", () => { card: { subTitle: "Mastercard, *1234" }, }); - it("formats and returns the cipher data", () => { + it("formats and returns the cipher data", async () => { overlayBackground["overlayLoginCiphers"] = new Map([ ["overlay-cipher-0", cipher2], ["overlay-cipher-1", cipher1], @@ -282,7 +297,7 @@ describe("OverlayBackground", () => { ["overlay-cipher-3", cipher4], ]); - const overlayCipherData = overlayBackground["getOverlayCipherData"](); + const overlayCipherData = await overlayBackground["getOverlayCipherData"](); expect(overlayCipherData).toStrictEqual([ { diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 30a52f2a06..297fee3592 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1,10 +1,10 @@ import { firstValueFrom } from "rxjs"; -import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -92,7 +92,7 @@ class OverlayBackground implements OverlayBackgroundInterface { private autofillService: AutofillService, private authService: AuthService, private environmentService: EnvironmentService, - private settingsService: SettingsService, + private domainSettingsService: DomainSettingsService, private stateService: StateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, @@ -145,7 +145,7 @@ class OverlayBackground implements OverlayBackgroundInterface { this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); } - const ciphers = this.getOverlayCipherData(); + const ciphers = await this.getOverlayCipherData(); this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers }); await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", { isOverlayCiphersPopulated: Boolean(ciphers.length), @@ -156,8 +156,8 @@ class OverlayBackground implements OverlayBackgroundInterface { * Strips out unnecessary data from the ciphers and returns an array of * objects that contain the cipher data needed for the overlay list. */ - private getOverlayCipherData(): OverlayCipherData[] { - const isFaviconDisabled = this.settingsService.getDisableFavicon(); + private async getOverlayCipherData(): Promise { + const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); const overlayCiphersArray = Array.from(this.overlayLoginCiphers); const overlayCipherData = []; let loginCipherIcon: WebsiteIconData; @@ -165,7 +165,7 @@ class OverlayBackground implements OverlayBackgroundInterface { for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) { const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex]; if (!loginCipherIcon && cipher.type === CipherType.Login) { - loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, isFaviconDisabled); + loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons); } overlayCipherData.push({ @@ -177,7 +177,7 @@ class OverlayBackground implements OverlayBackgroundInterface { icon: cipher.type === CipherType.Login ? loginCipherIcon - : buildCipherIcon(this.iconsServerUrl, cipher, isFaviconDisabled), + : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null, card: cipher.type === CipherType.Card ? cipher.card.subTitle : null, }); @@ -699,7 +699,7 @@ class OverlayBackground implements OverlayBackgroundInterface { styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), theme: await firstValueFrom(this.themeStateService.selectedTheme$), translations: this.getTranslations(), - ciphers: isOverlayListPort ? this.getOverlayCipherData() : null, + ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, }); this.updateOverlayPosition({ overlayElement: isOverlayListPort diff --git a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts index bbbca2f16a..d62e485722 100644 --- a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts +++ b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts @@ -6,6 +6,7 @@ import { EventCollectionServiceInitOptions, eventCollectionServiceFactory, } from "../../../background/service-factories/event-collection-service.factory"; +import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory"; import { CachedServices, factory, @@ -69,6 +70,7 @@ export function autofillServiceFactory( await logServiceFactory(cache, opts), await domainSettingsServiceFactory(cache, opts), await userVerificationServiceFactory(cache, opts), + await billingAccountProfileStateServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts index b827788d75..67637da2fd 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts @@ -3,6 +3,7 @@ import { of } from "rxjs"; import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -18,6 +19,7 @@ describe("context-menu", () => { let autofillSettingsService: MockProxy; let i18nService: MockProxy; let logService: MockProxy; + let billingAccountProfileStateService: MockProxy; let removeAllSpy: jest.SpyInstance void]>; let createSpy: jest.SpyInstance< @@ -32,6 +34,7 @@ describe("context-menu", () => { autofillSettingsService = mock(); i18nService = mock(); logService = mock(); + billingAccountProfileStateService = mock(); removeAllSpy = jest .spyOn(chrome.contextMenus, "removeAll") @@ -50,6 +53,7 @@ describe("context-menu", () => { autofillSettingsService, i18nService, logService, + billingAccountProfileStateService, ); autofillSettingsService.enableContextMenu$ = of(true); }); @@ -66,7 +70,7 @@ describe("context-menu", () => { }); it("has menu enabled, but does not have premium", async () => { - stateService.getCanAccessPremium.mockResolvedValue(false); + billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); const createdMenu = await sut.init(); expect(createdMenu).toBeTruthy(); @@ -74,7 +78,7 @@ describe("context-menu", () => { }); it("has menu enabled and has premium", async () => { - stateService.getCanAccessPremium.mockResolvedValue(true); + billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); const createdMenu = await sut.init(); expect(createdMenu).toBeTruthy(); @@ -128,7 +132,7 @@ describe("context-menu", () => { }); it("create entry for each cipher piece", async () => { - stateService.getCanAccessPremium.mockResolvedValue(true); + billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); await sut.loadOptions("TEST_TITLE", "1", createCipher()); @@ -137,7 +141,7 @@ describe("context-menu", () => { }); it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => { - stateService.getCanAccessPremium.mockResolvedValue(true); + billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); await sut.loadOptions("TEST_TITLE", "NOOP"); diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index 998b5c7258..9422756e07 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -17,6 +17,7 @@ import { SEPARATOR_ID, } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; @@ -27,6 +28,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory"; import { Account } from "../../models/account"; +import { billingAccountProfileStateServiceFactory } from "../../platform/background/service-factories/billing-account-profile-state-service.factory"; import { CachedServices } from "../../platform/background/service-factories/factory-options"; import { i18nServiceFactory, @@ -163,6 +165,7 @@ export class MainContextMenuHandler { private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, private logService: LogService, + private billingAccountProfileStateService: BillingAccountProfileStateService, ) {} static async mv3Create(cachedServices: CachedServices) { @@ -184,6 +187,11 @@ export class MainContextMenuHandler { stateServiceOptions: { stateFactory: stateFactory, }, + platformUtilsServiceOptions: { + clipboardWriteCallback: () => Promise.resolve(), + biometricCallback: () => Promise.resolve(false), + win: self, + }, }; return new MainContextMenuHandler( @@ -191,6 +199,7 @@ export class MainContextMenuHandler { await autofillSettingsServiceFactory(cachedServices, serviceOptions), await i18nServiceFactory(cachedServices, serviceOptions), await logServiceFactory(cachedServices, serviceOptions), + await billingAccountProfileStateServiceFactory(cachedServices, serviceOptions), ); } @@ -212,7 +221,10 @@ export class MainContextMenuHandler { try { for (const options of this.initContextMenuItems) { - if (options.checkPremiumAccess && !(await this.stateService.getCanAccessPremium())) { + if ( + options.checkPremiumAccess && + !(await firstValueFrom(this.billingAccountProfileStateService.hasPremiumFromAnySource$)) + ) { continue; } @@ -307,7 +319,9 @@ export class MainContextMenuHandler { await createChildItem(COPY_USERNAME_ID); } - const canAccessPremium = await this.stateService.getCanAccessPremium(); + const canAccessPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$, + ); if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) { await createChildItem(COPY_VERIFICATION_CODE_ID); } diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts index 8106f2698d..5cba4e0c0e 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts @@ -1,8 +1,15 @@ import AutofillOverlayButtonIframe from "./autofill-overlay-button-iframe"; -import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; describe("AutofillOverlayButtonIframe", () => { - window.customElements.define("autofill-overlay-button-iframe", AutofillOverlayButtonIframe); + window.customElements.define( + "autofill-overlay-button-iframe", + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayButtonIframe(this); + } + }, + ); afterAll(() => { jest.clearAllMocks(); @@ -13,7 +20,7 @@ describe("AutofillOverlayButtonIframe", () => { const iframe = document.querySelector("autofill-overlay-button-iframe"); - expect(iframe).toBeInstanceOf(AutofillOverlayButtonIframe); - expect(iframe).toBeInstanceOf(AutofillOverlayIframeElement); + expect(iframe).toBeInstanceOf(HTMLElement); + expect(iframe.shadowRoot).toBeDefined(); }); }); diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts index 813d805470..4f5d64b3cb 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts @@ -3,8 +3,9 @@ import { AutofillOverlayPort } from "../../utils/autofill-overlay.enum"; import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; class AutofillOverlayButtonIframe extends AutofillOverlayIframeElement { - constructor() { + constructor(element: HTMLElement) { super( + element, "overlay/button.html", AutofillOverlayPort.Button, { diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts index 7af5d973a9..71f7a290de 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts @@ -4,7 +4,21 @@ import AutofillOverlayIframeService from "./autofill-overlay-iframe.service"; jest.mock("./autofill-overlay-iframe.service"); describe("AutofillOverlayIframeElement", () => { - window.customElements.define("autofill-overlay-iframe", AutofillOverlayIframeElement); + window.customElements.define( + "autofill-overlay-iframe", + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayIframeElement( + this, + "overlay/button.html", + "overlay/button", + { background: "transparent", border: "none" }, + "bitwardenOverlayButton", + ); + } + }, + ); afterAll(() => { jest.clearAllMocks(); diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts index 209834410f..ed61c1eb8f 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts @@ -1,16 +1,15 @@ import AutofillOverlayIframeService from "./autofill-overlay-iframe.service"; -class AutofillOverlayIframeElement extends HTMLElement { +class AutofillOverlayIframeElement { constructor( + element: HTMLElement, iframePath: string, portName: string, initStyles: Partial, iframeTitle: string, ariaAlert?: string, ) { - super(); - - const shadow: ShadowRoot = this.attachShadow({ mode: "closed" }); + const shadow: ShadowRoot = element.attachShadow({ mode: "closed" }); const autofillOverlayIframeService = new AutofillOverlayIframeService( iframePath, portName, diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts index 5ba39eb002..ec89697e75 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts @@ -1,8 +1,15 @@ -import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; import AutofillOverlayListIframe from "./autofill-overlay-list-iframe"; describe("AutofillOverlayListIframe", () => { - window.customElements.define("autofill-overlay-list-iframe", AutofillOverlayListIframe); + window.customElements.define( + "autofill-overlay-list-iframe", + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayListIframe(this); + } + }, + ); afterAll(() => { jest.clearAllMocks(); @@ -13,7 +20,7 @@ describe("AutofillOverlayListIframe", () => { const iframe = document.querySelector("autofill-overlay-list-iframe"); - expect(iframe).toBeInstanceOf(AutofillOverlayListIframe); - expect(iframe).toBeInstanceOf(AutofillOverlayIframeElement); + expect(iframe).toBeInstanceOf(HTMLElement); + expect(iframe.shadowRoot).toBeDefined(); }); }); diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts index b60b618e4e..23df658154 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts @@ -3,8 +3,9 @@ import { AutofillOverlayPort } from "../../utils/autofill-overlay.enum"; import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; class AutofillOverlayListIframe extends AutofillOverlayIframeElement { - constructor() { + constructor(element: HTMLElement) { super( + element, "overlay/list.html", AutofillOverlayPort.List, { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 8926f5b298..9f3ffea142 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -877,6 +877,44 @@ describe("AutofillOverlayContentService", () => { sender: "autofillOverlayContentService", }); }); + + it("builds the overlay elements as custom web components if the user's browser is not Firefox", () => { + let namesIndex = 0; + const customNames = ["op-autofill-overlay-button", "op-autofill-overlay-list"]; + + jest + .spyOn(autofillOverlayContentService as any, "generateRandomCustomElementName") + .mockImplementation(() => { + if (namesIndex > 1) { + return ""; + } + const customName = customNames[namesIndex]; + namesIndex++; + + return customName; + }); + autofillOverlayContentService["isFirefoxBrowser"] = false; + + autofillOverlayContentService.openAutofillOverlay(); + + expect(autofillOverlayContentService["overlayButtonElement"]).toBeInstanceOf(HTMLElement); + expect(autofillOverlayContentService["overlayButtonElement"].tagName).toEqual( + customNames[0].toUpperCase(), + ); + expect(autofillOverlayContentService["overlayListElement"]).toBeInstanceOf(HTMLElement); + expect(autofillOverlayContentService["overlayListElement"].tagName).toEqual( + customNames[1].toUpperCase(), + ); + }); + + it("builds the overlay elements as `div` elements if the user's browser is Firefox", () => { + autofillOverlayContentService["isFirefoxBrowser"] = true; + + autofillOverlayContentService.openAutofillOverlay(); + + expect(autofillOverlayContentService["overlayButtonElement"]).toBeInstanceOf(HTMLDivElement); + expect(autofillOverlayContentService["overlayListElement"]).toBeInstanceOf(HTMLDivElement); + }); }); describe("focusMostRecentOverlayField", () => { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 2cf063a5ba..79abdc3938 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -30,6 +30,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte isOverlayCiphersPopulated = false; pageDetailsUpdateRequired = false; autofillOverlayVisibility: number; + private isFirefoxBrowser = + globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 || + globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1; + private readonly generateRandomCustomElementName = generateRandomCustomElementName; private readonly findTabs = tabbable; private readonly sendExtensionMessage = sendExtensionMessage; private formFieldElements: Set> = new Set([]); @@ -593,6 +597,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private updateOverlayButtonPosition() { if (!this.overlayButtonElement) { this.createAutofillOverlayButton(); + this.updateCustomElementDefaultStyles(this.overlayButtonElement); } if (!this.isOverlayButtonVisible) { @@ -613,6 +618,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private updateOverlayListPosition() { if (!this.overlayListElement) { this.createAutofillOverlayList(); + this.updateCustomElementDefaultStyles(this.overlayListElement); } if (!this.isOverlayListVisible) { @@ -765,11 +771,24 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte return; } - const customElementName = generateRandomCustomElementName(); - globalThis.customElements?.define(customElementName, AutofillOverlayButtonIframe); - this.overlayButtonElement = globalThis.document.createElement(customElementName); + if (this.isFirefoxBrowser) { + this.overlayButtonElement = globalThis.document.createElement("div"); + new AutofillOverlayButtonIframe(this.overlayButtonElement); - this.updateCustomElementDefaultStyles(this.overlayButtonElement); + return; + } + + const customElementName = this.generateRandomCustomElementName(); + globalThis.customElements?.define( + customElementName, + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayButtonIframe(this); + } + }, + ); + this.overlayButtonElement = globalThis.document.createElement(customElementName); } /** @@ -781,11 +800,24 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte return; } - const customElementName = generateRandomCustomElementName(); - globalThis.customElements?.define(customElementName, AutofillOverlayListIframe); - this.overlayListElement = globalThis.document.createElement(customElementName); + if (this.isFirefoxBrowser) { + this.overlayListElement = globalThis.document.createElement("div"); + new AutofillOverlayListIframe(this.overlayListElement); - this.updateCustomElementDefaultStyles(this.overlayListElement); + return; + } + + const customElementName = this.generateRandomCustomElementName(); + globalThis.customElements?.define( + customElementName, + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayListIframe(this); + } + }, + ); + this.overlayListElement = globalThis.document.createElement(customElementName); } /** diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 2a9519292e..f6c1fa9067 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -8,6 +8,7 @@ import { DefaultDomainSettingsService, DomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -72,6 +73,7 @@ describe("AutofillService", () => { const eventCollectionService = mock(); const logService = mock(); const userVerificationService = mock(); + const billingAccountProfileStateService = mock(); beforeEach(() => { autofillService = new AutofillService( @@ -83,6 +85,7 @@ describe("AutofillService", () => { logService, domainSettingsService, userVerificationService, + billingAccountProfileStateService, ); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); @@ -476,6 +479,7 @@ describe("AutofillService", () => { it("throws an error if an autofill did not occur for any of the passed pages", async () => { autofillOptions.tab.url = "https://a-different-url.com"; + billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); try { await autofillService.doAutoFill(autofillOptions); @@ -487,7 +491,6 @@ describe("AutofillService", () => { }); it("will autofill login data for a page", async () => { - jest.spyOn(stateService, "getCanAccessPremium"); jest.spyOn(autofillService as any, "generateFillScript"); jest.spyOn(autofillService as any, "generateLoginFillScript"); jest.spyOn(logService, "info"); @@ -497,8 +500,6 @@ describe("AutofillService", () => { const autofillResult = await autofillService.doAutoFill(autofillOptions); const currentAutofillPageDetails = autofillOptions.pageDetails[0]; - expect(stateService.getCanAccessPremium).toHaveBeenCalled(); - expect(autofillService["getDefaultUriMatchStrategy"]).toHaveBeenCalled(); expect(autofillService["generateFillScript"]).toHaveBeenCalledWith( currentAutofillPageDetails.details, { @@ -660,7 +661,7 @@ describe("AutofillService", () => { it("returns a TOTP value", async () => { const totpCode = "123456"; autofillOptions.cipher.login.totp = "totp"; - jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(true); + billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true); jest.spyOn(totpService, "getCode").mockResolvedValue(totpCode); @@ -673,7 +674,7 @@ describe("AutofillService", () => { it("does not return a TOTP value if the user does not have premium features", async () => { autofillOptions.cipher.login.totp = "totp"; - jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(false); + billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true); const autofillResult = await autofillService.doAutoFill(autofillOptions); @@ -707,7 +708,7 @@ describe("AutofillService", () => { it("returns a null value if the user cannot access premium and the organization does not use TOTP", async () => { autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.organizationUseTotp = false; - jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValueOnce(false); + billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); const autofillResult = await autofillService.doAutoFill(autofillOptions); @@ -717,13 +718,12 @@ describe("AutofillService", () => { it("returns a null value if the user has disabled `auto TOTP copy`", async () => { autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.organizationUseTotp = true; - jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(true); + billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false); jest.spyOn(totpService, "getCode"); const autofillResult = await autofillService.doAutoFill(autofillOptions); - expect(stateService.getCanAccessPremium).toHaveBeenCalled(); expect(autofillService.getShouldAutoCopyTotp).toHaveBeenCalled(); expect(totpService.getCode).not.toHaveBeenCalled(); expect(autofillResult).toBeNull(); @@ -3380,6 +3380,34 @@ describe("AutofillService", () => { expect(value).toBe(false); }); + + it("validates attribute identifiers with mixed camel case and non-alpha characters", () => { + const attributes: Record = { + _$1_go_look: true, + go_look: true, + goLook: true, + go1look: true, + "go look": true, + look_go: true, + findPerson: true, + query$1: true, + look_goo: false, + golook: false, + lookgo: false, + logonField: false, + ego_input: false, + "Gold Password": false, + searching_for: false, + person_finder: false, + }; + const autofillFieldMocks = Object.keys(attributes).map((key) => + createAutofillFieldMock({ htmlID: key }), + ); + autofillFieldMocks.forEach((field) => { + const value = AutofillService["isSearchField"](field); + expect(value).toBe(attributes[field.htmlID]); + }); + }); }); describe("isFieldMatch", () => { diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 3cb73dd72b..e353a34ea0 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -5,6 +5,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { UriMatchStrategySetting, @@ -44,6 +45,7 @@ export default class AutofillService implements AutofillServiceInterface { private openPasswordRepromptPopoutDebounce: NodeJS.Timeout; private currentlyOpeningPasswordRepromptPopout = false; private autofillScriptPortsSet = new Set(); + static searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames); constructor( private cipherService: CipherService, @@ -54,6 +56,7 @@ export default class AutofillService implements AutofillServiceInterface { private logService: LogService, private domainSettingsService: DomainSettingsService, private userVerificationService: UserVerificationService, + private billingAccountProfileStateService: BillingAccountProfileStateService, ) {} /** @@ -239,7 +242,9 @@ export default class AutofillService implements AutofillServiceInterface { let totp: string | null = null; - const canAccessPremium = await this.stateService.getCanAccessPremium(); + const canAccessPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$, + ); const defaultUriMatch = await this.getDefaultUriMatchStrategy(); if (!canAccessPremium) { @@ -1380,11 +1385,33 @@ export default class AutofillService implements AutofillServiceInterface { return excludedTypes.indexOf(type) > -1; } + /** + * Identifies if a passed field contains text artifacts that identify it as a search field. + * + * @param field - The autofill field that we are validating as a search field + */ private static isSearchField(field: AutofillField) { const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder]; - const matchPattern = new RegExp(AutoFillConstants.SearchFieldNames.join("|"), "gi"); + for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) { + if (!matchFieldAttributeValues[attrIndex]) { + continue; + } - return Boolean(matchFieldAttributeValues.join(" ").match(matchPattern)); + // Separate camel case words and case them to lower case values + const camelCaseSeparatedFieldAttribute = matchFieldAttributeValues[attrIndex] + .replace(/([a-z])([A-Z])/g, "$1 $2") + .toLowerCase(); + // Split the attribute by non-alphabetical characters to get the keywords + const attributeKeywords = camelCaseSeparatedFieldAttribute.split(/[^a-z]/gi); + + for (let keywordIndex = 0; keywordIndex < attributeKeywords.length; keywordIndex++) { + if (AutofillService.searchFieldNamesSet.has(attributeKeywords[keywordIndex])) { + return true; + } + } + } + + return false; } static isExcludedFieldType(field: AutofillField, excludedTypes: string[]) { @@ -1397,11 +1424,7 @@ export default class AutofillService implements AutofillServiceInterface { } // Check if the input is an untyped/mistyped search input - if (this.isSearchField(field)) { - return true; - } - - return false; + return this.isSearchField(field); } /** @@ -1525,11 +1548,7 @@ export default class AutofillService implements AutofillServiceInterface { return false; } - if (AutoFillConstants.PasswordFieldExcludeList.some((i) => cleanedValue.indexOf(i) > -1)) { - return false; - } - - return true; + return !AutoFillConstants.PasswordFieldExcludeList.some((i) => cleanedValue.indexOf(i) > -1); } static fieldHasDisqualifyingAttributeValue(field: AutofillField) { @@ -1572,7 +1591,11 @@ export default class AutofillService implements AutofillServiceInterface { const arr: AutofillField[] = []; pageDetails.fields.forEach((f) => { - if (AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillLoginTypes)) { + const isPassword = f.type === "password"; + if ( + !isPassword && + AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillLoginTypes) + ) { return; } @@ -1581,23 +1604,16 @@ export default class AutofillService implements AutofillServiceInterface { return; } - const isPassword = f.type === "password"; - const isLikePassword = () => { if (f.type !== "text") { return false; } - if (AutofillService.valueIsLikePassword(f.htmlID)) { - return true; - } - - if (AutofillService.valueIsLikePassword(f.htmlName)) { - return true; - } - - if (AutofillService.valueIsLikePassword(f.placeholder)) { - return true; + const testedValues = [f.htmlID, f.htmlName, f.placeholder]; + for (let i = 0; i < testedValues.length; i++) { + if (AutofillService.valueIsLikePassword(testedValues[i])) { + return true; + } } return false; diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index f623e0f6c9..1de801a2c2 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -755,6 +755,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte // Prioritize capturing text content from elements rather than nodes. currentElement = currentElement.parentElement || currentElement.parentNode; + if (!currentElement) { + return textContentItems; + } let siblingElement = nodeIsElement(currentElement) ? currentElement.previousElementSibling diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index c63d25c364..5ea1284d1b 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -553,17 +553,30 @@ describe("InsertAutofillContentService", () => { insertAutofillContentService as any, "simulateUserMouseClickAndFocusEventInteractions", ); + jest.spyOn(targetInput, "blur"); insertAutofillContentService["handleFocusOnFieldByOpidAction"]("__0"); expect( insertAutofillContentService["collectAutofillContentService"].getAutofillFieldElementByOpid, ).toBeCalledWith("__0"); + expect(targetInput.blur).not.toHaveBeenCalled(); expect( insertAutofillContentService["simulateUserMouseClickAndFocusEventInteractions"], ).toHaveBeenCalledWith(targetInput, true); expect(elementEventCount).toEqual(expectedElementEventCount); }); + + it("blurs the element if it is currently the active element before simulating click and focus events", () => { + const targetInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + targetInput.opid = "__0"; + targetInput.focus(); + jest.spyOn(targetInput, "blur"); + + insertAutofillContentService["handleFocusOnFieldByOpidAction"]("__0"); + + expect(targetInput.blur).toHaveBeenCalled(); + }); }); describe("insertValueIntoField", () => { @@ -710,7 +723,7 @@ describe("InsertAutofillContentService", () => { }); describe("triggerPostInsertEventsOnElement", () => { - it("triggers simulated event interactions and blurs the element after", () => { + it("triggers simulated event interactions", () => { const elementValue = "test"; document.body.innerHTML = ``; const element = document.getElementById("username") as FillableFormFieldElement; @@ -726,7 +739,6 @@ describe("InsertAutofillContentService", () => { expect(insertAutofillContentService["simulateInputElementChangedEvent"]).toHaveBeenCalledWith( element, ); - expect(element.blur).toHaveBeenCalled(); expect(element.value).toBe(elementValue); }); }); diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index c5b763d77d..dd14cadfa7 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -185,11 +185,18 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf /** * Handles finding an element by opid and triggering click and focus events on the element. - * @param {string} opid - * @private + * To ensure that we trigger a blur event correctly on a filled field, we first check if the + * element is already focused. If it is, we blur the element before focusing on it again. + * + * @param {string} opid - The opid of the element to focus on. */ private handleFocusOnFieldByOpidAction(opid: string) { const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); + + if (document.activeElement === element) { + element.blur(); + } + this.simulateUserMouseClickAndFocusEventInteractions(element, true); } @@ -282,7 +289,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf } this.simulateInputElementChangedEvent(element); - element.blur(); } /** @@ -379,10 +385,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf element.dispatchEvent(new Event(simulatedInputEvents[index], { bubbles: true })); } } - - private nodeIsElement(node: Node): node is HTMLElement { - return node.nodeType === Node.ELEMENT_NODE; - } } export default InsertAutofillContentService; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index dc3daf71a1..c84cced018 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -8,19 +8,18 @@ import { AuthRequestServiceAbstraction, AuthRequestService, } from "@bitwarden/auth/common"; -import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; -import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; @@ -39,6 +38,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; +import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; @@ -64,6 +64,8 @@ import { UserNotificationSettingsService, UserNotificationSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/user-notification-settings.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -117,7 +119,6 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service"; /* eslint-enable import/no-restricted-paths */ import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service"; import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; @@ -125,6 +126,7 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; +import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/avatar.service"; import { PasswordGenerationService, PasswordGenerationServiceAbstraction, @@ -180,7 +182,6 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; -import { BrowserOrganizationService } from "../admin-console/services/browser-organization.service"; import ContextMenusBackground from "../autofill/background/context-menus.background"; import NotificationBackground from "../autofill/background/notification.background"; import OverlayBackground from "../autofill/background/overlay.background"; @@ -211,7 +212,6 @@ import { BrowserPlatformUtilsService } from "../platform/services/platform-utils import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserSendService } from "../services/browser-send.service"; -import { BrowserSettingsService } from "../services/browser-settings.service"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service"; @@ -240,7 +240,6 @@ export default class MainBackground { appIdService: AppIdServiceAbstraction; apiService: ApiServiceAbstraction; environmentService: BrowserEnvironmentService; - settingsService: SettingsServiceAbstraction; cipherService: CipherServiceAbstraction; folderService: InternalFolderServiceAbstraction; collectionService: CollectionServiceAbstraction; @@ -288,7 +287,7 @@ export default class MainBackground { fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction; fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction; fido2ClientService: Fido2ClientServiceAbstraction; - avatarUpdateService: AvatarUpdateServiceAbstraction; + avatarService: AvatarServiceAbstraction; mainContextMenuHandler: MainContextMenuHandler; cipherContextMenuHandler: CipherContextMenuHandler; configService: BrowserConfigService; @@ -311,6 +310,7 @@ export default class MainBackground { biometricStateService: BiometricStateService; stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; + billingAccountProfileStateService: BillingAccountProfileStateService; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -409,8 +409,7 @@ export default class MainBackground { ); this.activeUserStateProvider = new DefaultActiveUserStateProvider( this.accountService, - storageServiceProvider, - stateEventRegistrarService, + this.singleUserStateProvider, ); this.derivedStateProvider = new BackgroundDerivedStateProvider( this.memoryStorageForStateProviders, @@ -428,6 +427,21 @@ export default class MainBackground { ); this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); + this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider); + this.platformUtilsService = new BackgroundPlatformUtilsService( + this.messagingService, + (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), + async () => this.biometricUnlock(), + self, + ); + + this.tokenService = new TokenService( + this.singleUserStateProvider, + this.globalStateProvider, + this.platformUtilsService.supportsSecureStorage(), + this.secureStorageService, + ); + const migrationRunner = new MigrationRunner( this.storageService, this.logService, @@ -442,15 +456,9 @@ export default class MainBackground { new StateFactory(GlobalState, Account), this.accountService, this.environmentService, + this.tokenService, migrationRunner, ); - this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider); - this.platformUtilsService = new BackgroundPlatformUtilsService( - this.messagingService, - (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), - async () => this.biometricUnlock(), - self, - ); const themeStateService = new DefaultThemeStateService(this.globalStateProvider); @@ -466,17 +474,17 @@ export default class MainBackground { this.stateProvider, this.biometricStateService, ); - this.tokenService = new TokenService(this.stateService); + this.appIdService = new AppIdService(this.globalStateProvider); this.apiService = new ApiService( this.tokenService, this.platformUtilsService, this.environmentService, this.appIdService, + this.stateService, (expired: boolean) => this.logout(expired), ); this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); - this.settingsService = new BrowserSettingsService(this.stateService); this.fileUploadService = new FileUploadService(this.logService); this.cipherFileUploadService = new CipherFileUploadService( this.apiService, @@ -490,10 +498,7 @@ export default class MainBackground { this.stateProvider, ); this.syncNotifierService = new SyncNotifierService(); - this.organizationService = new BrowserOrganizationService( - this.stateService, - this.stateProvider, - ); + this.organizationService = new OrganizationService(this.stateProvider); this.policyService = new PolicyService(this.stateProvider, this.organizationService); this.autofillSettingsService = new AutofillSettingsService( this.stateProvider, @@ -563,6 +568,10 @@ export default class MainBackground { this.stateService, ); + this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( + this.activeUserStateProvider, + ); + this.loginStrategyService = new LoginStrategyService( this.cryptoService, this.apiService, @@ -582,6 +591,7 @@ export default class MainBackground { this.deviceTrustCryptoService, this.authRequestService, this.globalStateProvider, + this.billingAccountProfileStateService, ); this.ssoLoginService = new SsoLoginService(this.stateProvider); @@ -685,7 +695,11 @@ export default class MainBackground { this.fileUploadService, this.sendService, ); + + this.avatarService = new AvatarService(this.apiService, this.stateProvider); + this.providerService = new ProviderService(this.stateProvider); + this.syncService = new SyncService( this.apiService, this.domainSettingsService, @@ -703,18 +717,22 @@ export default class MainBackground { this.folderApiService, this.organizationService, this.sendApiService, + this.avatarService, logoutCallback, + this.billingAccountProfileStateService, ); this.eventUploadService = new EventUploadService( this.apiService, - this.stateService, + this.stateProvider, this.logService, + this.accountService, ); this.eventCollectionService = new EventCollectionService( this.cipherService, - this.stateService, + this.stateProvider, this.organizationService, this.eventUploadService, + this.accountService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); @@ -727,6 +745,7 @@ export default class MainBackground { this.logService, this.domainSettingsService, this.userVerificationService, + this.billingAccountProfileStateService, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); @@ -787,7 +806,6 @@ export default class MainBackground { this.fido2AuthenticatorService, this.configService, this.authService, - this.stateService, this.vaultSettingsService, this.domainSettingsService, this.logService, @@ -835,7 +853,6 @@ export default class MainBackground { this.cryptoService, this.cryptoFunctionService, this.runtimeBackground, - this.i18nService, this.messagingService, this.appIdService, this.platformUtilsService, @@ -869,7 +886,7 @@ export default class MainBackground { this.autofillService, this.authService, this.environmentService, - this.settingsService, + this.domainSettingsService, this.stateService, this.autofillSettingsService, this.i18nService, @@ -943,14 +960,13 @@ export default class MainBackground { this.apiService, ); - this.avatarUpdateService = new AvatarUpdateService(this.apiService, this.stateService); - if (!this.popupOnlyContext) { this.mainContextMenuHandler = new MainContextMenuHandler( this.stateService, this.autofillSettingsService, this.i18nService, this.logService, + this.billingAccountProfileStateService, ); this.cipherContextMenuHandler = new CipherContextMenuHandler( @@ -1090,7 +1106,7 @@ export default class MainBackground { async logout(expired: boolean, userId?: UserId) { userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.eventUploadService.uploadEvents(userId); + await this.eventUploadService.uploadEvents(userId as UserId); await Promise.all([ this.syncService.setLastSync(new Date(0), userId), diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 620f9735c8..240fb1dede 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -5,7 +5,6 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -75,7 +74,6 @@ export class NativeMessagingBackground { private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, private runtimeBackground: RuntimeBackground, - private i18nService: I18nService, private messagingService: MessagingService, private appIdService: AppIdService, private platformUtilsService: PlatformUtilsService, diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 6c0c0f169a..f422fc8550 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -97,6 +97,10 @@ export default class RuntimeBackground { case "unlocked": { let item: LockedVaultPendingNotificationsData; + if (msg.command === "loggedIn") { + await this.sendBwInstalledMessageToVault(); + } + if (this.lockedVaultPendingNotifications?.length > 0) { item = this.lockedVaultPendingNotifications.pop(); await closeUnlockPopout(); @@ -130,9 +134,6 @@ export default class RuntimeBackground { await this.main.refreshBadge(); await this.main.refreshMenu(); }, 2000); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.main.avatarUpdateService.loadColorFromState(); this.configService.triggerServerConfigFetch(); } break; @@ -354,8 +355,6 @@ export default class RuntimeBackground { if (await this.environmentService.hasManagedEnvironment()) { await this.environmentService.setUrlsToManagedEnvironment(); } - - await this.sendBwInstalledMessageToVault(); } this.onInstalledReason = null; diff --git a/apps/browser/src/background/service-factories/event-collection-service.factory.ts b/apps/browser/src/background/service-factories/event-collection-service.factory.ts index 7ce77da045..ec892c73dd 100644 --- a/apps/browser/src/background/service-factories/event-collection-service.factory.ts +++ b/apps/browser/src/background/service-factories/event-collection-service.factory.ts @@ -5,15 +5,14 @@ import { organizationServiceFactory, OrganizationServiceInitOptions, } from "../../admin-console/background/service-factories/organization-service.factory"; +import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory"; import { FactoryOptions, CachedServices, factory, } from "../../platform/background/service-factories/factory-options"; -import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../platform/background/service-factories/state-service.factory"; +import { stateProviderFactory } from "../../platform/background/service-factories/state-provider.factory"; +import { StateServiceInitOptions } from "../../platform/background/service-factories/state-service.factory"; import { cipherServiceFactory, CipherServiceInitOptions, @@ -43,9 +42,10 @@ export function eventCollectionServiceFactory( async () => new EventCollectionService( await cipherServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), await organizationServiceFactory(cache, opts), await eventUploadServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/service-factories/event-upload-service.factory.ts b/apps/browser/src/background/service-factories/event-upload-service.factory.ts index fcaec459c0..4e1d7949be 100644 --- a/apps/browser/src/background/service-factories/event-upload-service.factory.ts +++ b/apps/browser/src/background/service-factories/event-upload-service.factory.ts @@ -1,6 +1,7 @@ import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; +import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory"; import { ApiServiceInitOptions, apiServiceFactory, @@ -14,10 +15,8 @@ import { logServiceFactory, LogServiceInitOptions, } from "../../platform/background/service-factories/log-service.factory"; -import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../platform/background/service-factories/state-service.factory"; +import { stateProviderFactory } from "../../platform/background/service-factories/state-provider.factory"; +import { StateServiceInitOptions } from "../../platform/background/service-factories/state-service.factory"; type EventUploadServiceOptions = FactoryOptions; @@ -37,8 +36,9 @@ export function eventUploadServiceFactory( async () => new EventUploadService( await apiServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), await logServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/service-factories/settings-service.factory.ts b/apps/browser/src/background/service-factories/settings-service.factory.ts deleted file mode 100644 index 28e97de51f..0000000000 --- a/apps/browser/src/background/service-factories/settings-service.factory.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { SettingsService as AbstractSettingsService } from "@bitwarden/common/abstractions/settings.service"; - -import { - FactoryOptions, - CachedServices, - factory, -} from "../../platform/background/service-factories/factory-options"; -import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../platform/background/service-factories/state-service.factory"; -import { BrowserSettingsService } from "../../services/browser-settings.service"; - -type SettingsServiceFactoryOptions = FactoryOptions; - -export type SettingsServiceInitOptions = SettingsServiceFactoryOptions & StateServiceInitOptions; - -export function settingsServiceFactory( - cache: { settingsService?: AbstractSettingsService } & CachedServices, - opts: SettingsServiceInitOptions, -): Promise { - return factory( - cache, - "settingsService", - opts, - async () => new BrowserSettingsService(await stateServiceFactory(cache, opts)), - ); -} diff --git a/apps/browser/src/platform/background/service-factories/active-user-state-provider.factory.ts b/apps/browser/src/platform/background/service-factories/active-user-state-provider.factory.ts index 6dafd2952e..ff46ca84e8 100644 --- a/apps/browser/src/platform/background/service-factories/active-user-state-provider.factory.ts +++ b/apps/browser/src/platform/background/service-factories/active-user-state-provider.factory.ts @@ -9,20 +9,15 @@ import { import { CachedServices, FactoryOptions, factory } from "./factory-options"; import { - StateEventRegistrarServiceInitOptions, - stateEventRegistrarServiceFactory, -} from "./state-event-registrar-service.factory"; -import { - StorageServiceProviderInitOptions, - storageServiceProviderFactory, -} from "./storage-service-provider.factory"; + SingleUserStateProviderInitOptions, + singleUserStateProviderFactory, +} from "./single-user-state-provider.factory"; type ActiveUserStateProviderFactory = FactoryOptions; export type ActiveUserStateProviderInitOptions = ActiveUserStateProviderFactory & AccountServiceInitOptions & - StorageServiceProviderInitOptions & - StateEventRegistrarServiceInitOptions; + SingleUserStateProviderInitOptions; export async function activeUserStateProviderFactory( cache: { activeUserStateProvider?: ActiveUserStateProvider } & CachedServices, @@ -35,8 +30,7 @@ export async function activeUserStateProviderFactory( async () => new DefaultActiveUserStateProvider( await accountServiceFactory(cache, opts), - await storageServiceProviderFactory(cache, opts), - await stateEventRegistrarServiceFactory(cache, opts), + await singleUserStateProviderFactory(cache, opts), ), ); } diff --git a/apps/browser/src/platform/background/service-factories/api-service.factory.ts b/apps/browser/src/platform/background/service-factories/api-service.factory.ts index 649fe1f7fe..57cd500441 100644 --- a/apps/browser/src/platform/background/service-factories/api-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/api-service.factory.ts @@ -20,6 +20,7 @@ import { PlatformUtilsServiceInitOptions, platformUtilsServiceFactory, } from "./platform-utils-service.factory"; +import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; type ApiServiceFactoryOptions = FactoryOptions & { apiServiceOptions: { @@ -32,7 +33,8 @@ export type ApiServiceInitOptions = ApiServiceFactoryOptions & TokenServiceInitOptions & PlatformUtilsServiceInitOptions & EnvironmentServiceInitOptions & - AppIdServiceInitOptions; + AppIdServiceInitOptions & + StateServiceInitOptions; export function apiServiceFactory( cache: { apiService?: AbstractApiService } & CachedServices, @@ -48,6 +50,7 @@ export function apiServiceFactory( await platformUtilsServiceFactory(cache, opts), await environmentServiceFactory(cache, opts), await appIdServiceFactory(cache, opts), + await stateServiceFactory(cache, opts), opts.apiServiceOptions.logoutCallback, opts.apiServiceOptions.customUserAgent, ), diff --git a/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts b/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts new file mode 100644 index 0000000000..80482eacb6 --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts @@ -0,0 +1,28 @@ +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; + +import { activeUserStateProviderFactory } from "./active-user-state-provider.factory"; +import { FactoryOptions, CachedServices, factory } from "./factory-options"; +import { StateProviderInitOptions } from "./state-provider.factory"; + +type BillingAccountProfileStateServiceFactoryOptions = FactoryOptions; + +export type BillingAccountProfileStateServiceInitOptions = + BillingAccountProfileStateServiceFactoryOptions & StateProviderInitOptions; + +export function billingAccountProfileStateServiceFactory( + cache: { + billingAccountProfileStateService?: BillingAccountProfileStateService; + } & CachedServices, + opts: BillingAccountProfileStateServiceInitOptions, +): Promise { + return factory( + cache, + "billingAccountProfileStateService", + opts, + async () => + new DefaultBillingAccountProfileStateService( + await activeUserStateProviderFactory(cache, opts), + ), + ); +} diff --git a/apps/browser/src/platform/background/service-factories/state-service.factory.ts b/apps/browser/src/platform/background/service-factories/state-service.factory.ts index 8bcb65a320..20a9ac074a 100644 --- a/apps/browser/src/platform/background/service-factories/state-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/state-service.factory.ts @@ -5,6 +5,10 @@ import { accountServiceFactory, AccountServiceInitOptions, } from "../../../auth/background/service-factories/account-service.factory"; +import { + tokenServiceFactory, + TokenServiceInitOptions, +} from "../../../auth/background/service-factories/token-service.factory"; import { Account } from "../../../models/account"; import { BrowserStateService } from "../../services/browser-state.service"; @@ -38,6 +42,7 @@ export type StateServiceInitOptions = StateServiceFactoryOptions & LogServiceInitOptions & AccountServiceInitOptions & EnvironmentServiceInitOptions & + TokenServiceInitOptions & MigrationRunnerInitOptions; export async function stateServiceFactory( @@ -57,6 +62,7 @@ export async function stateServiceFactory( opts.stateServiceOptions.stateFactory, await accountServiceFactory(cache, opts), await environmentServiceFactory(cache, opts), + await tokenServiceFactory(cache, opts), await migrationRunnerFactory(cache, opts), opts.stateServiceOptions.useAccountCache, ), diff --git a/apps/browser/src/platform/popup/locales.ts b/apps/browser/src/platform/popup/locales.ts index f24f7368db..5fea00b97b 100644 --- a/apps/browser/src/platform/popup/locales.ts +++ b/apps/browser/src/platform/popup/locales.ts @@ -7,6 +7,7 @@ import localeBn from "@angular/common/locales/bn"; import localeBs from "@angular/common/locales/bs"; import localeCa from "@angular/common/locales/ca"; import localeCs from "@angular/common/locales/cs"; +import localeCy from "@angular/common/locales/cy"; import localeDa from "@angular/common/locales/da"; import localeDe from "@angular/common/locales/de"; import localeEl from "@angular/common/locales/el"; @@ -19,6 +20,7 @@ import localeFa from "@angular/common/locales/fa"; import localeFi from "@angular/common/locales/fi"; import localeFil from "@angular/common/locales/fil"; import localeFr from "@angular/common/locales/fr"; +import localeGl from "@angular/common/locales/gl"; import localeHe from "@angular/common/locales/he"; import localeHi from "@angular/common/locales/hi"; import localeHr from "@angular/common/locales/hr"; @@ -33,9 +35,13 @@ import localeKo from "@angular/common/locales/ko"; import localeLt from "@angular/common/locales/lt"; import localeLv from "@angular/common/locales/lv"; import localeMl from "@angular/common/locales/ml"; +import localeMr from "@angular/common/locales/mr"; +import localeMy from "@angular/common/locales/my"; import localeNb from "@angular/common/locales/nb"; +import localeNe from "@angular/common/locales/ne"; import localeNl from "@angular/common/locales/nl"; import localeNn from "@angular/common/locales/nn"; +import localeOr from "@angular/common/locales/or"; import localePl from "@angular/common/locales/pl"; import localePtBr from "@angular/common/locales/pt"; import localePtPt from "@angular/common/locales/pt-PT"; @@ -46,6 +52,7 @@ import localeSk from "@angular/common/locales/sk"; import localeSl from "@angular/common/locales/sl"; import localeSr from "@angular/common/locales/sr"; import localeSv from "@angular/common/locales/sv"; +import localeTe from "@angular/common/locales/te"; import localeTh from "@angular/common/locales/th"; import localeTr from "@angular/common/locales/tr"; import localeUk from "@angular/common/locales/uk"; @@ -61,6 +68,7 @@ registerLocaleData(localeBn, "bn"); registerLocaleData(localeBs, "bs"); registerLocaleData(localeCa, "ca"); registerLocaleData(localeCs, "cs"); +registerLocaleData(localeCy, "cy"); registerLocaleData(localeDa, "da"); registerLocaleData(localeDe, "de"); registerLocaleData(localeEl, "el"); @@ -73,6 +81,7 @@ registerLocaleData(localeFa, "fa"); registerLocaleData(localeFi, "fi"); registerLocaleData(localeFil, "fil"); registerLocaleData(localeFr, "fr"); +registerLocaleData(localeGl, "gl"); registerLocaleData(localeHe, "he"); registerLocaleData(localeHi, "hi"); registerLocaleData(localeHr, "hr"); @@ -87,9 +96,13 @@ registerLocaleData(localeKo, "ko"); registerLocaleData(localeLt, "lt"); registerLocaleData(localeLv, "lv"); registerLocaleData(localeMl, "ml"); +registerLocaleData(localeMr, "mr"); +registerLocaleData(localeMy, "my"); registerLocaleData(localeNb, "nb"); +registerLocaleData(localeNe, "ne"); registerLocaleData(localeNl, "nl"); registerLocaleData(localeNn, "nn"); +registerLocaleData(localeOr, "or"); registerLocaleData(localePl, "pl"); registerLocaleData(localePtBr, "pt-BR"); registerLocaleData(localePtPt, "pt-PT"); @@ -100,6 +113,7 @@ registerLocaleData(localeSk, "sk"); registerLocaleData(localeSl, "sl"); registerLocaleData(localeSr, "sr"); registerLocaleData(localeSv, "sv"); +registerLocaleData(localeTe, "te"); registerLocaleData(localeTh, "th"); registerLocaleData(localeTr, "tr"); registerLocaleData(localeUk, "uk"); diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index b5d1b9c38a..3069b8f174 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -1,5 +1,6 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { @@ -32,6 +33,7 @@ describe("Browser State Service", () => { let stateFactory: MockProxy>; let useAccountCache: boolean; let environmentService: MockProxy; + let tokenService: MockProxy; let migrationRunner: MockProxy; let state: State; @@ -46,6 +48,7 @@ describe("Browser State Service", () => { logService = mock(); stateFactory = mock(); environmentService = mock(); + tokenService = mock(); migrationRunner = mock(); // turn off account cache for tests useAccountCache = false; @@ -77,6 +80,7 @@ describe("Browser State Service", () => { stateFactory, accountService, environmentService, + tokenService, migrationRunner, useAccountCache, ); diff --git a/apps/browser/src/platform/services/browser-state.service.ts b/apps/browser/src/platform/services/browser-state.service.ts index c544915e26..f7ee74be21 100644 --- a/apps/browser/src/platform/services/browser-state.service.ts +++ b/apps/browser/src/platform/services/browser-state.service.ts @@ -1,6 +1,7 @@ import { BehaviorSubject } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { @@ -45,6 +46,7 @@ export class BrowserStateService stateFactory: StateFactory, accountService: AccountService, environmentService: EnvironmentService, + tokenService: TokenService, migrationRunner: MigrationRunner, useAccountCache = true, ) { @@ -56,6 +58,7 @@ export class BrowserStateService stateFactory, accountService, environmentService, + tokenService, migrationRunner, useAccountCache, ); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 5f97b57882..52de0303fa 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -17,11 +17,8 @@ import { LoginStrategyServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; -import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -42,6 +39,10 @@ import { AutofillSettingsService, AutofillSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { + DefaultDomainSettingsService, + DomainSettingsService, +} from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsService, UserNotificationSettingsServiceAbstraction, @@ -98,7 +99,6 @@ import { DialogService } from "@bitwarden/components"; import { ImportServiceAbstraction } from "@bitwarden/importer/core"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; -import { BrowserOrganizationService } from "../../admin-console/services/browser-organization.service"; import { UnauthGuardService } from "../../auth/popup/services"; import { AutofillService } from "../../autofill/services/abstractions/autofill.service"; import MainBackground from "../../background/main.background"; @@ -118,7 +118,6 @@ import { ForegroundPlatformUtilsService } from "../../platform/services/platform import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { BrowserSendService } from "../../services/browser-send.service"; -import { BrowserSettingsService } from "../../services/browser-settings.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { VaultFilterService } from "../../vault/services/vault-filter.service"; @@ -233,7 +232,6 @@ function getBgService(service: keyof MainBackground) { deps: [], }, { provide: TotpService, useFactory: getBgService("totpService"), deps: [] }, - { provide: TokenService, useFactory: getBgService("tokenService"), deps: [] }, { provide: I18nServiceAbstraction, useFactory: (globalStateProvider: GlobalStateProvider) => { @@ -265,16 +263,6 @@ function getBgService(service: keyof MainBackground) { useFactory: getBgService("devicesService"), deps: [], }, - { - provide: EventUploadService, - useFactory: getBgService("eventUploadService"), - deps: [], - }, - { - provide: EventCollectionService, - useFactory: getBgService("eventCollectionService"), - deps: [], - }, { provide: PlatformUtilsService, useExisting: ForegroundPlatformUtilsService, @@ -348,11 +336,9 @@ function getBgService(service: keyof MainBackground) { }, { provide: SyncService, useFactory: getBgService("syncService"), deps: [] }, { - provide: SettingsService, - useFactory: (stateService: StateServiceAbstraction) => { - return new BrowserSettingsService(stateService); - }, - deps: [StateServiceAbstraction], + provide: DomainSettingsService, + useClass: DefaultDomainSettingsService, + deps: [StateProvider], }, { provide: AbstractStorageService, @@ -399,13 +385,6 @@ function getBgService(service: keyof MainBackground) { useFactory: getBgService("notificationsService"), deps: [], }, - { - provide: OrganizationService, - useFactory: (stateService: StateServiceAbstraction, stateProvider: StateProvider) => { - return new BrowserOrganizationService(stateService, stateProvider); - }, - deps: [StateServiceAbstraction, StateProvider], - }, { provide: VaultFilterService, useClass: VaultFilterService, @@ -445,6 +424,7 @@ function getBgService(service: keyof MainBackground) { logService: LogServiceAbstraction, accountService: AccountServiceAbstraction, environmentService: EnvironmentService, + tokenService: TokenService, migrationRunner: MigrationRunner, ) => { return new BrowserStateService( @@ -455,6 +435,7 @@ function getBgService(service: keyof MainBackground) { new StateFactory(GlobalState, Account), accountService, environmentService, + tokenService, migrationRunner, ); }, @@ -465,6 +446,7 @@ function getBgService(service: keyof MainBackground) { LogServiceAbstraction, AccountServiceAbstraction, EnvironmentService, + TokenService, MigrationRunner, ], }, diff --git a/apps/browser/src/popup/settings/excluded-domains.component.ts b/apps/browser/src/popup/settings/excluded-domains.component.ts index 6fa28d10ab..1741fbaa65 100644 --- a/apps/browser/src/popup/settings/excluded-domains.component.ts +++ b/apps/browser/src/popup/settings/excluded-domains.component.ts @@ -6,7 +6,6 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -31,7 +30,6 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy { accountSwitcherEnabled = false; constructor( - private stateService: StateService, private domainSettingsService: DomainSettingsService, private i18nService: I18nService, private router: Router, diff --git a/apps/browser/src/popup/settings/options.component.ts b/apps/browser/src/popup/settings/options.component.ts index 813eeda144..dbdb94c586 100644 --- a/apps/browser/src/popup/settings/options.component.ts +++ b/apps/browser/src/popup/settings/options.component.ts @@ -1,7 +1,6 @@ import { Component, OnInit } from "@angular/core"; import { firstValueFrom } from "rxjs"; -import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; @@ -13,7 +12,6 @@ import { } from "@bitwarden/common/models/domain/domain-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; @@ -51,14 +49,12 @@ export class OptionsComponent implements OnInit { constructor( private messagingService: MessagingService, - private stateService: StateService, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction, private domainSettingsService: DomainSettingsService, private badgeSettingsService: BadgeSettingsServiceAbstraction, i18nService: I18nService, private themeStateService: ThemeStateService, - private settingsService: SettingsService, private vaultSettingsService: VaultSettingsService, ) { this.themeOptions = [ @@ -114,12 +110,14 @@ export class OptionsComponent implements OnInit { this.autofillSettingsService.enableContextMenu$, ); - this.showCardsCurrentTab = !(await this.stateService.getDontShowCardsCurrentTab()); - this.showIdentitiesCurrentTab = !(await this.stateService.getDontShowIdentitiesCurrentTab()); + this.showCardsCurrentTab = await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$); + this.showIdentitiesCurrentTab = await firstValueFrom( + this.vaultSettingsService.showIdentitiesCurrentTab$, + ); this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$); - this.enableFavicon = !this.settingsService.getDisableFavicon(); + this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$); this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$); @@ -169,7 +167,7 @@ export class OptionsComponent implements OnInit { } async updateFavicon() { - await this.settingsService.setDisableFavicon(!this.enableFavicon); + await this.domainSettingsService.setShowFavicons(this.enableFavicon); } async updateBadgeCounter() { @@ -178,11 +176,11 @@ export class OptionsComponent implements OnInit { } async updateShowCardsCurrentTab() { - await this.stateService.setDontShowCardsCurrentTab(!this.showCardsCurrentTab); + await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab); } async updateShowIdentitiesCurrentTab() { - await this.stateService.setDontShowIdentitiesCurrentTab(!this.showIdentitiesCurrentTab); + await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab); } async saveTheme() { diff --git a/apps/browser/src/popup/settings/premium.component.html b/apps/browser/src/popup/settings/premium.component.html index 2727ee405b..a8f9855e62 100644 --- a/apps/browser/src/popup/settings/premium.component.html +++ b/apps/browser/src/popup/settings/premium.component.html @@ -12,7 +12,7 @@
- +

{{ "premiumNotCurrentMember" | i18n }}

{{ "premiumSignUpAndGet" | i18n }}

    @@ -61,7 +61,7 @@ > - +

    {{ "premiumCurrentMember" | i18n }}

    {{ "premiumCurrentMemberThanks" | i18n }}

    -
    +