diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 23f4bd35f10..f924c5c98ea 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -164,6 +164,10 @@ jobs: run: npm run dist:mv3 working-directory: browser-source/apps/browser + - name: Build Chrome Manifest v3 Beta + run: npm run dist:chrome:beta + working-directory: browser-source/apps/browser + - name: Gulp run: gulp ci working-directory: browser-source/apps/browser @@ -196,6 +200,13 @@ jobs: path: browser-source/apps/browser/dist/dist-chrome-mv3.zip if-no-files-found: error + - name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD) + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip + if-no-files-found: error + - name: Upload Firefox artifact uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 6a5d9f14057..b034136f585 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -230,6 +230,17 @@ jobs: url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + update-summary: + name: Display commit + needs: artifact-check + runs-on: ubuntu-22.04 + steps: + - name: Display commit SHA + run: | + REPO_URL="https://github.com/bitwarden/clients/commit" + COMMIT_SHA="${{ needs.artifact-check.outputs.artifact-build-commit }}" + echo ":steam_locomotive: View [commit]($REPO_URL/$COMMIT_SHA)" >> $GITHUB_STEP_SUMMARY + azure-deploy: name: Deploy Web Vault to ${{ inputs.environment }} Storage Account needs: diff --git a/.storybook/main.ts b/.storybook/main.ts index c71a74c2a73..cb63ada550b 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -9,6 +9,8 @@ const config: StorybookConfig = { "../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)", "../apps/web/src/**/*.mdx", "../apps/web/src/**/*.stories.@(js|jsx|ts|tsx)", + "../apps/browser/src/**/*.mdx", + "../apps/browser/src/**/*.stories.@(js|jsx|ts|tsx)", "../bitwarden_license/bit-web/src/**/*.mdx", "../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)", ], diff --git a/.storybook/tsconfig.json b/.storybook/tsconfig.json index 113cc5bcde5..34acc9a740c 100644 --- a/.storybook/tsconfig.json +++ b/.storybook/tsconfig.json @@ -1,12 +1,10 @@ { "extends": "../tsconfig", "compilerOptions": { - "types": ["node", "jest", "chrome"], "allowSyntheticDefaultImports": true }, - "exclude": ["../src/test.setup.ts", "../apps/src/**/*.spec.ts", "../libs/**/*.spec.ts"], + "exclude": ["../src/test.setup.ts", "../apps/**/*.spec.ts", "../libs/**/*.spec.ts"], "files": [ - "./typings.d.ts", "./preview.tsx", "../libs/components/src/main.ts", "../libs/components/src/polyfills.ts" diff --git a/.storybook/typings.d.ts b/.storybook/typings.d.ts deleted file mode 100644 index c94d67b1a26..00000000000 --- a/.storybook/typings.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.md" { - const content: string; - export default content; -} diff --git a/apps/browser/gulpfile.js b/apps/browser/gulpfile.js index 6a0980fc27f..d5b29ffc388 100644 --- a/apps/browser/gulpfile.js +++ b/apps/browser/gulpfile.js @@ -35,6 +35,9 @@ function buildString() { if (process.env.MANIFEST_VERSION) { build = `-mv${process.env.MANIFEST_VERSION}`; } + if (process.env.BETA_BUILD === "1") { + build += "-beta"; + } if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") { build = `-${process.env.BUILD_NUMBER}`; } @@ -65,6 +68,9 @@ function distFirefox() { manifest.optional_permissions = manifest.optional_permissions.filter( (permission) => permission !== "privacy", ); + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -72,6 +78,9 @@ function distFirefox() { function distOpera() { return dist("opera", (manifest) => { delete manifest.applications; + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -81,6 +90,9 @@ function distChrome() { delete manifest.applications; delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -90,6 +102,9 @@ function distEdge() { delete manifest.applications; delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -210,6 +225,9 @@ async function safariCopyBuild(source, dest) { delete manifest.commands._execute_sidebar_action; delete manifest.optional_permissions; manifest.permissions.push("nativeMessaging"); + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }), ), @@ -235,6 +253,19 @@ async function ciCoverage(cb) { .pipe(gulp.dest(paths.coverage)); } +function applyBetaLabels(manifest) { + manifest.name = "Bitwarden Password Manager BETA"; + manifest.short_name = "Bitwarden BETA"; + manifest.description = "THIS EXTENSION IS FOR BETA TESTING BITWARDEN."; + if (process.env.GITHUB_RUN_ID) { + manifest.version_name = `${manifest.version} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`; + manifest.version = `${manifest.version}.${parseInt(process.env.GITHUB_RUN_ID.slice(-4))}`; + } else { + manifest.version = `${manifest.version}.0`; + } + return manifest; +} + exports["dist:firefox"] = distFirefox; exports["dist:chrome"] = distChrome; exports["dist:opera"] = distOpera; diff --git a/apps/browser/package.json b/apps/browser/package.json index 506f19f279b..580acfc3d02 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -7,10 +7,14 @@ "build:watch": "webpack --watch", "build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch", "build:prod": "cross-env NODE_ENV=production webpack", + "build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack", "build:prod:watch": "cross-env NODE_ENV=production webpack --watch", "dist": "npm run build:prod && gulp dist", + "dist:beta": "npm run build:prod:beta && cross-env BETA_BUILD=1 gulp dist", "dist:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist", + "dist:mv3:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist", "dist:chrome": "npm run build:prod && gulp dist:chrome", + "dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome", "dist:firefox": "npm run build:prod && gulp dist:firefox", "dist:opera": "npm run build:prod && gulp dist:opera", "dist:safari": "npm run build:prod && gulp dist:safari", diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index d3d9106c15e..1a56d32a35a 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza Twoje hasła, passkeys i poufne informacje", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 417bc977ebe..0f40bc63bb0 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Em qual lugar for, o Bitwarden protege suas senhas, chaves de acesso, e informações confidenciais", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 4eba4ffaeaa..6e530412db6 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - Trình Quản lý Mật khẩu", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Ở nhà, ở cơ quan, hay trên đường đi, Bitwarden sẽ bảo mật tất cả mật khẩu, passkey, và thông tin cá nhân của bạn", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -650,7 +650,7 @@ "message": "'Thông báo Thêm đăng nhập' sẽ tự động nhắc bạn lưu các đăng nhập mới vào hầm an toàn của bạn bất cứ khi nào bạn đăng nhập trang web lần đầu tiên." }, "addLoginNotificationDescAlt": { - "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "showCardsCurrentTab": { "message": "Hiển thị thẻ trên trang Tab" @@ -685,13 +685,13 @@ "message": "Yêu cầu cập nhật mật khẩu đăng nhập khi phát hiện thay đổi trên trang web." }, "changedPasswordNotificationDescAlt": { - "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để cập nhật mật khẩu khi phát hiện có sự thay đổi trên trang web. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "Đưa ra lựa chọn để lưu và sử dụng passkey" }, "usePasskeysDesc": { - "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để lưu passkey mới hoặc đăng nhập bằng passkey đã lưu trong hòm. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "notificationChangeDesc": { "message": "Bạn có muốn cập nhật mật khẩu này trên Bitwarden không?" @@ -712,7 +712,7 @@ "message": "Sử dụng một đúp chuột để truy cập vào việc tạo mật khẩu và thông tin đăng nhập phù hợp cho trang web. " }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "Truy cập trình khởi tạo mật khẩu và các mục đăng nhập đã lưu của trang web bằng cách nhấn đúp chuột. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "defaultUriMatchDetection": { "message": "Phương thức kiểm tra URI mặc định", @@ -728,7 +728,7 @@ "message": "Thay đổi màu sắc ứng dụng." }, "themeDescAlt": { - "message": "Change the application's color theme. Applies to all logged in accounts." + "message": "Thay đổi tông màu giao diện của ứng dụng. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "dark": { "message": "Tối", @@ -1061,10 +1061,10 @@ "message": "Tắt cài đặt trình quản lý mật khẩu tích hợp trong trình duyệt của bạn để tránh xung đột." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { - "message": "Edit browser settings." + "message": "Thay đổi cài đặt của trình duyệt." }, "autofillOverlayVisibilityOff": { - "message": "Off", + "message": "Tắt", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { @@ -1168,7 +1168,7 @@ "message": "Hiển thị một ảnh nhận dạng bên cạnh mỗi lần đăng nhập." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "Hiển thị một biểu tượng dễ nhận dạng bên cạnh mỗi mục đăng nhập. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "enableBadgeCounter": { "message": "Hiển thị biểu tượng bộ đếm" @@ -1500,7 +1500,7 @@ "message": "Mã PIN không hợp lệ." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Mã PIN bị gõ sai quá nhiều lần. Đang đăng xuất." }, "unlockWithBiometrics": { "message": "Mở khóa bằng sinh trắc học" diff --git a/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts b/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts index 1d79bbbaf1d..5af5eb00177 100644 --- a/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts @@ -1,11 +1,13 @@ import { TwoFactorService as AbstractTwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; import { FactoryOptions, CachedServices, factory, } from "../../../platform/background/service-factories/factory-options"; +import { globalStateProviderFactory } from "../../../platform/background/service-factories/global-state-provider.factory"; import { I18nServiceInitOptions, i18nServiceFactory, @@ -19,7 +21,8 @@ type TwoFactorServiceFactoryOptions = FactoryOptions; export type TwoFactorServiceInitOptions = TwoFactorServiceFactoryOptions & I18nServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + GlobalStateProvider; export async function twoFactorServiceFactory( cache: { twoFactorService?: AbstractTwoFactorService } & CachedServices, @@ -33,6 +36,7 @@ export async function twoFactorServiceFactory( new TwoFactorService( await i18nServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await globalStateProviderFactory(cache, opts), ), ); service.init(); 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 a73ec3e1f67..e5a3b8f8f54 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 @@ -110,7 +110,7 @@ export class AccountSwitcherService { }), ); - // Create a reusable observable that listens to the the switchAccountFinish message and returns the userId from the message + // Create a reusable observable that listens to the switchAccountFinish message and returns the userId from the message this.switchAccountFinished$ = fromChromeEvent<[message: { command: string; userId: string }]>( chrome.runtime.onMessage, ).pipe( 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 bad2e4a9e77..6191d277add 100644 --- a/apps/browser/src/auth/popup/two-factor-options.component.ts +++ b/apps/browser/src/auth/popup/two-factor-options.component.ts @@ -2,7 +2,10 @@ import { Component } from "@angular/core"; 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 { + TwoFactorProviderDetails, + 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"; @@ -27,9 +30,9 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { this.navigateTo2FA(); } - choose(p: any) { - super.choose(p); - this.twoFactorService.setSelectedProvider(p.type); + override async choose(p: TwoFactorProviderDetails) { + await super.choose(p); + await this.twoFactorService.setSelectedProvider(p.type); this.navigateTo2FA(); } diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 8f85d65692f..10e2d84361b 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -130,7 +130,9 @@ export default class AutofillService implements AutofillServiceInterface { if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) { injectedScripts.push("autofiller.js"); - } else { + } + + if (!triggeringOnPageLoad) { await this.scriptInjectorService.inject({ tabId: tab.id, injectDetails: { file: "content/content-message-handler.js", runAt: "document_start" }, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index b4375df7d50..5aec6e01a4b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,10 +1,8 @@ -import { Subject, firstValueFrom, merge } from "rxjs"; +import { Subject, firstValueFrom, merge, timeout } from "rxjs"; import { PinCryptoServiceAbstraction, PinCryptoService, - LoginStrategyServiceAbstraction, - LoginStrategyService, InternalUserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsService, AuthRequestServiceAbstraction, @@ -38,7 +36,6 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; -import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -54,7 +51,6 @@ import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connect import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; -import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { @@ -75,6 +71,7 @@ import { } 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 { ClientType } from "@bitwarden/common/enums"; 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -277,7 +274,6 @@ export default class MainBackground { containerService: ContainerService; auditService: AuditServiceAbstraction; authService: AuthServiceAbstraction; - loginStrategyService: LoginStrategyServiceAbstraction; loginEmailService: LoginEmailServiceAbstraction; importApiService: ImportApiServiceAbstraction; importService: ImportServiceAbstraction; @@ -301,7 +297,6 @@ export default class MainBackground { providerService: ProviderServiceAbstraction; keyConnectorService: KeyConnectorServiceAbstraction; userVerificationService: UserVerificationServiceAbstraction; - twoFactorService: TwoFactorServiceAbstraction; vaultFilterService: VaultFilterService; usernameGenerationService: UsernameGenerationServiceAbstraction; encryptService: EncryptService; @@ -496,7 +491,7 @@ export default class MainBackground { this.accountService, this.singleUserStateProvider, ); - this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider); + this.derivedStateProvider = new BackgroundDerivedStateProvider(); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, this.singleUserStateProvider, @@ -526,6 +521,7 @@ export default class MainBackground { this.storageService, this.logService, new MigrationBuilderService(), + ClientType.Browser, ); this.stateService = new DefaultBrowserStateService( @@ -614,8 +610,6 @@ export default class MainBackground { this.stateService, ); - this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); @@ -659,32 +653,6 @@ export default class MainBackground { this.loginEmailService = new LoginEmailService(this.stateProvider); - this.loginStrategyService = new LoginStrategyService( - this.accountService, - this.masterPasswordService, - this.cryptoService, - this.apiService, - this.tokenService, - this.appIdService, - this.platformUtilsService, - this.messagingService, - this.logService, - this.keyConnectorService, - this.environmentService, - this.stateService, - this.twoFactorService, - this.i18nService, - this.encryptService, - this.passwordStrengthService, - this.policyService, - this.deviceTrustService, - this.authRequestService, - this.userDecryptionOptionsService, - this.globalStateProvider, - this.billingAccountProfileStateService, - this.kdfConfigService, - ); - this.ssoLoginService = new SsoLoginService(this.stateProvider); this.userVerificationApiService = new UserVerificationApiService(this.apiService); @@ -1114,8 +1082,7 @@ export default class MainBackground { this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); await (this.i18nService as I18nService).init(); - await (this.eventUploadService as EventUploadService).init(true); - this.twoFactorService.init(); + (this.eventUploadService as EventUploadService).init(true); if (this.popupOnlyContext) { return; @@ -1231,7 +1198,18 @@ export default class MainBackground { } async logout(expired: boolean, userId?: UserId) { - userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; + userId ??= ( + await firstValueFrom( + this.accountService.activeAccount$.pipe( + timeout({ + first: 2000, + with: () => { + throw new Error("No active account found to logout"); + }, + }), + ), + ) + )?.id; await this.eventUploadService.uploadEvents(userId as UserId); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 294346fe9f9..14eb228fb0a 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -85,7 +85,11 @@ export default class RuntimeBackground { this.messageListener.allMessages$ .pipe( mergeMap(async (message: any) => { - await this.processMessage(message); + try { + await this.processMessage(message); + } catch (err) { + this.logService.error(err); + } }), ) .subscribe(); diff --git a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts index 4025d01950f..3c3900144bb 100644 --- a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts +++ b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts @@ -3,15 +3,10 @@ import { DerivedStateProvider } from "@bitwarden/common/platform/state"; import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider"; import { CachedServices, FactoryOptions, factory } from "./factory-options"; -import { - StorageServiceProviderInitOptions, - storageServiceProviderFactory, -} from "./storage-service-provider.factory"; type DerivedStateProviderFactoryOptions = FactoryOptions; -export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions & - StorageServiceProviderInitOptions; +export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions; export async function derivedStateProviderFactory( cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices, @@ -21,7 +16,6 @@ export async function derivedStateProviderFactory( cache, "derivedStateProvider", opts, - async () => - new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)), + async () => new BackgroundDerivedStateProvider(), ); } diff --git a/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts b/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts index a49699a6158..090531f7cfc 100644 --- a/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts +++ b/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts @@ -1,3 +1,4 @@ +import { ClientType } from "@bitwarden/common/enums"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -27,6 +28,7 @@ export async function migrationRunnerFactory( await diskStorageServiceFactory(cache, opts), await logServiceFactory(cache, opts), new MigrationBuilderService(), + ClientType.Browser, ), ); } diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index b793777d8b6..f536eb8312d 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -238,10 +238,6 @@ export class BrowserApi { return typeof window !== "undefined" && window === BrowserApi.getBackgroundPage(); } - static getApplicationVersion(): string { - return chrome.runtime.getManifest().version; - } - /** * Gets the extension views that match the given properties. This method is not * available within background service worker. As a result, it will return an diff --git a/apps/browser/src/platform/popup/browser-popup-utils.spec.ts b/apps/browser/src/platform/popup/browser-popup-utils.spec.ts index e84cd19a45f..c2d33369bd6 100644 --- a/apps/browser/src/platform/popup/browser-popup-utils.spec.ts +++ b/apps/browser/src/platform/popup/browser-popup-utils.spec.ts @@ -203,7 +203,7 @@ describe("BrowserPopupUtils", () => { expect(BrowserPopupUtils["buildPopoutUrl"]).not.toHaveBeenCalled(); }); - it("replaces any existing `uilocation=` query params within the passed extension url path to state the the uilocaiton is a popup", async () => { + it("replaces any existing `uilocation=` query params within the passed extension url path to state the uilocation is a popup", async () => { const url = "popup/index.html?uilocation=sidebar#/tabs/vault"; jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false); diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.html b/apps/browser/src/platform/popup/layout/popup-footer.component.html new file mode 100644 index 00000000000..2cbbca79c0b --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.html @@ -0,0 +1,9 @@ + diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.ts b/apps/browser/src/platform/popup/layout/popup-footer.component.ts new file mode 100644 index 00000000000..826a1d1c601 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.ts @@ -0,0 +1,9 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "popup-footer", + templateUrl: "popup-footer.component.html", + standalone: true, + imports: [], +}) +export class PopupFooterComponent {} diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.html b/apps/browser/src/platform/popup/layout/popup-header.component.html new file mode 100644 index 00000000000..c0894f8168b --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-header.component.html @@ -0,0 +1,19 @@ +
+
+
+ +

{{ pageTitle }}

+
+
+ +
+
+
diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.ts b/apps/browser/src/platform/popup/layout/popup-header.component.ts new file mode 100644 index 00000000000..f2f8eb95af0 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-header.component.ts @@ -0,0 +1,34 @@ +import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion"; +import { CommonModule, Location } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { IconButtonModule, TypographyModule } from "@bitwarden/components"; + +@Component({ + selector: "popup-header", + templateUrl: "popup-header.component.html", + standalone: true, + imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule], +}) +export class PopupHeaderComponent { + /** Display the back button, which uses Location.back() to go back one page in history */ + @Input() + get showBackButton() { + return this._showBackButton; + } + set showBackButton(value: BooleanInput) { + this._showBackButton = coerceBooleanProperty(value); + } + + private _showBackButton = false; + + /** Title string that will be inserted as an h1 */ + @Input({ required: true }) pageTitle: string; + + constructor(private location: Location) {} + + back() { + this.location.back(); + } +} diff --git a/apps/browser/src/platform/popup/layout/popup-layout.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx new file mode 100644 index 00000000000..91f7dab277e --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx @@ -0,0 +1,138 @@ +import { Meta, Story, Canvas } from "@storybook/addon-docs"; + +import * as stories from "./popup-layout.stories"; + + + +Please note that because these stories use `router-outlet`, there are issues with rendering content +when Light & Dark mode is selected. The stories are best viewed by selecting one color mode. + +# Popup Tab Navigation + +The popup tab navigation component composes together the popup page and the bottom tab navigation +footer. This component is intended to be used a level _above_ each extension tab's page code. + +The navigation footer contains the 4 main page links for the browser extension. It uses the Angular +router to determine which page is currently active, and style the button appropriately. Clicking on +the buttons will navigate to the correct route. The navigation footer has a max-width built in so +that the page looks nice when the extension is popped out. + +Long button names will be ellipsed. + +Usage example: + +```html + + + +``` + +# Popup Page + +The popup page handles positioning a page's `header` and `footer` elements, and inserting the rest +of the content into the `main` element with scroll. There is also a max-width built in so that the +page looks nice when the extension is popped out. + +**Slots** + +- `header` + - Use `popup-header` component. + - Every page should have a header. +- `footer` + - Use the `popup-footer` component. + - Not every page will have a footer. +- default + - Whatever content you want in `main`. + +Basic usage example: + +```html + + +
This is content
+ +
+``` + +## Popup header + +**Args** + +- `pageTitle`: required + - Inserts title as an `h1`. +- `showBackButton`: optional, defaults to `false` + - Toggles the back button to appear. The back button uses `Location.back()` to navigate back one + page in history. + +**Slots** + +- `end` + - Use to insert one or more interactive elements. + - The header handles the spacing between elements passed to the `end` slot. + +Usage example: + +```html + + + + + + +``` + +Common interactive elements to insert into the `end` slot are: + +- `app-current-account`: shows current account and switcher +- `app-pop-out`: shows popout button when the extension is not already popped out +- "Add" button: this can be accomplished with the Button component and any custom functionality for + that particular page + +## Popup footer + +Popup footer should be used when the page displays action buttons. It functions similarly to the +Dialog footer in that the calling code is responsible for passing in the different buttons that need +to be rendered. + +Usage example: + +```html + + + + +``` + +# Page types + +There are a few types of pages that are used in the browser extension. + +View the story source code to see examples of how to construct these types of pages. + +## Extension Tab + +Example of wrapping an extension page in the `popup-tab-navigation` component. + + + + + +## Extension Page + +Examples of using just the `popup-page` component, without and with a footer. + + + + + + + + + +## Popped out + +When the browser extension is popped out, the "popout" button should not be passed to the header. + + + + diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts new file mode 100644 index 00000000000..1b10e50c0c2 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -0,0 +1,367 @@ +import { CommonModule } from "@angular/common"; +import { Component, importProvidersFrom } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + AvatarModule, + ButtonModule, + I18nMockService, + IconButtonModule, +} from "@bitwarden/components"; + +import { PopupFooterComponent } from "./popup-footer.component"; +import { PopupHeaderComponent } from "./popup-header.component"; +import { PopupPageComponent } from "./popup-page.component"; +import { PopupTabNavigationComponent } from "./popup-tab-navigation.component"; + +@Component({ + selector: "extension-container", + template: ` +
+ +
+ `, + standalone: true, +}) +class ExtensionContainerComponent {} + +@Component({ + selector: "vault-placeholder", + template: ` +
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item last item
+ `, + standalone: true, +}) +class VaultComponent {} + +@Component({ + selector: "generator-placeholder", + template: `
generator stuff here
`, + standalone: true, +}) +class GeneratorComponent {} + +@Component({ + selector: "send-placeholder", + template: `
send some stuff
`, + standalone: true, +}) +class SendComponent {} + +@Component({ + selector: "settings-placeholder", + template: `
change your settings
`, + standalone: true, +}) +class SettingsComponent {} + +@Component({ + selector: "mock-add-button", + template: ` + + `, + standalone: true, + imports: [ButtonModule], +}) +class MockAddButtonComponent {} + +@Component({ + selector: "mock-popout-button", + template: ` + + `, + standalone: true, + imports: [IconButtonModule], +}) +class MockPopoutButtonComponent {} + +@Component({ + selector: "mock-current-account", + template: ` + + `, + standalone: true, + imports: [AvatarModule], +}) +class MockCurrentAccountComponent {} + +@Component({ + selector: "mock-vault-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultPageComponent {} + +@Component({ + selector: "mock-vault-page-popped", + template: ` + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultPagePoppedComponent {} + +@Component({ + selector: "mock-generator-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + GeneratorComponent, + ], +}) +class MockGeneratorPageComponent {} + +@Component({ + selector: "mock-send-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + SendComponent, + ], +}) +class MockSendPageComponent {} + +@Component({ + selector: "mock-settings-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + SettingsComponent, + ], +}) +class MockSettingsPageComponent {} + +@Component({ + selector: "mock-vault-subpage", + template: ` + + + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + ButtonModule, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultSubpageComponent {} + +export default { + title: "Browser/Popup Layout", + component: PopupPageComponent, + decorators: [ + moduleMetadata({ + imports: [ + PopupTabNavigationComponent, + CommonModule, + RouterModule, + ExtensionContainerComponent, + MockVaultSubpageComponent, + MockVaultPageComponent, + MockSendPageComponent, + MockGeneratorPageComponent, + MockSettingsPageComponent, + MockVaultPagePoppedComponent, + ], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + back: "Back", + }); + }, + }, + ], + }), + applicationConfig({ + providers: [ + importProvidersFrom( + RouterModule.forRoot( + [ + { path: "", redirectTo: "vault", pathMatch: "full" }, + { path: "vault", component: MockVaultPageComponent }, + { path: "generator", component: MockGeneratorPageComponent }, + { path: "send", component: MockSendPageComponent }, + { path: "settings", component: MockSettingsPageComponent }, + // in case you are coming from a story that also uses the router + { path: "**", redirectTo: "vault" }, + ], + { useHash: true }, + ), + ), + ], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const PopupTabNavigation: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + + + `, + }), +}; + +export const PopupPage: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + `, + }), +}; + +export const PopupPageWithFooter: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + `, + }), +}; + +export const PoppedOut: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` +
+ +
+ `, + }), +}; diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html new file mode 100644 index 00000000000..ba871d6319e --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -0,0 +1,7 @@ + +
+
+ +
+
+ diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts new file mode 100644 index 00000000000..1223a6f4188 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "popup-page", + templateUrl: "popup-page.component.html", + standalone: true, + host: { + class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto", + }, +}) +export class PopupPageComponent {} diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html new file mode 100644 index 00000000000..a0ff252c6c2 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -0,0 +1,32 @@ +
+ +
+ diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts new file mode 100644 index 00000000000..3a275454d94 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -0,0 +1,43 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { LinkModule } from "@bitwarden/components"; + +@Component({ + selector: "popup-tab-navigation", + templateUrl: "popup-tab-navigation.component.html", + standalone: true, + imports: [CommonModule, LinkModule, RouterModule], + host: { + class: "tw-block tw-h-full tw-w-full tw-flex tw-flex-col", + }, +}) +export class PopupTabNavigationComponent { + navButtons = [ + { + label: "Vault", + page: "/vault", + iconKey: "lock", + iconKeyActive: "lock-f", + }, + { + label: "Generator", + page: "/generator", + iconKey: "generate", + iconKeyActive: "generate-f", + }, + { + label: "Send", + page: "/send", + iconKey: "send", + iconKeyActive: "send-f", + }, + { + label: "Settings", + page: "/settings", + iconKey: "cog", + iconKeyActive: "cog-f", + }, + ]; +} diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index e9f7f17d9bd..6e3b3aa4032 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -175,11 +175,13 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic } getApplicationVersion(): Promise { - return Promise.resolve(BrowserApi.getApplicationVersion()); + const manifest = chrome.runtime.getManifest(); + return Promise.resolve(manifest.version_name ?? manifest.version); } - async getApplicationVersionNumber(): Promise { - return (await this.getApplicationVersion()).split(RegExp("[+|-]"))[0].trim(); + getApplicationVersionNumber(): Promise { + const manifest = chrome.runtime.getManifest(); + return Promise.resolve(manifest.version.split(RegExp("[+|-]"))[0].trim()); } supportsWebAuthn(win: Window): boolean { diff --git a/apps/browser/src/platform/state/background-derived-state.provider.ts b/apps/browser/src/platform/state/background-derived-state.provider.ts index f3d217789ed..cbc5a34b37b 100644 --- a/apps/browser/src/platform/state/background-derived-state.provider.ts +++ b/apps/browser/src/platform/state/background-derived-state.provider.ts @@ -1,9 +1,5 @@ import { Observable } from "rxjs"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; @@ -16,14 +12,11 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, - storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState { - const [cacheKey, storageService] = storageLocation; return new BackgroundDerivedState( parentState$, deriveDefinition, - storageService, - cacheKey, + deriveDefinition.buildCacheKey(), dependencies, ); } diff --git a/apps/browser/src/platform/state/background-derived-state.ts b/apps/browser/src/platform/state/background-derived-state.ts index c62795acdcd..61768cb970c 100644 --- a/apps/browser/src/platform/state/background-derived-state.ts +++ b/apps/browser/src/platform/state/background-derived-state.ts @@ -1,10 +1,7 @@ -import { Observable, Subscription } from "rxjs"; +import { Observable, Subscription, concatMap } from "rxjs"; import { Jsonify } from "type-fest"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DeriveDefinition } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedState } from "@bitwarden/common/platform/state/implementations/default-derived-state"; @@ -22,11 +19,10 @@ export class BackgroundDerivedState< constructor( parentState$: Observable, deriveDefinition: DeriveDefinition, - memoryStorage: AbstractStorageService & ObservableStorageService, portName: string, dependencies: TDeps, ) { - super(parentState$, deriveDefinition, memoryStorage, dependencies); + super(parentState$, deriveDefinition, dependencies); // listen for foreground derived states to connect BrowserApi.addListener(chrome.runtime.onConnect, (port) => { @@ -42,7 +38,20 @@ export class BackgroundDerivedState< }); port.onMessage.addListener(listenerCallback); - const stateSubscription = this.state$.subscribe(); + const stateSubscription = this.state$ + .pipe( + concatMap(async (state) => { + await this.sendMessage( + { + action: "nextState", + data: JSON.stringify(state), + id: Utils.newGuid(), + }, + port, + ); + }), + ) + .subscribe(); this.portSubscriptions.set(port, stateSubscription); }); diff --git a/apps/browser/src/platform/state/derived-state-interactions.spec.ts b/apps/browser/src/platform/state/derived-state-interactions.spec.ts index a5df01bc989..823c071a4c5 100644 --- a/apps/browser/src/platform/state/derived-state-interactions.spec.ts +++ b/apps/browser/src/platform/state/derived-state-interactions.spec.ts @@ -4,14 +4,13 @@ */ import { NgZone } from "@angular/core"; -import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service"; -import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec/utils"; import { mock } from "jest-mock-extended"; import { Subject, firstValueFrom } from "rxjs"; import { DeriveDefinition } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition import { StateDefinition } from "@bitwarden/common/platform/state/state-definition"; +import { awaitAsync, trackEmissions, ObservableTracker } from "@bitwarden/common/spec"; import { mockPorts } from "../../../spec/mock-port.spec-util"; @@ -22,6 +21,7 @@ const stateDefinition = new StateDefinition("test", "memory"); const deriveDefinition = new DeriveDefinition(stateDefinition, "test", { derive: (dateString: string) => (dateString == null ? null : new Date(dateString)), deserializer: (dateString: string) => (dateString == null ? null : new Date(dateString)), + cleanupDelayMs: 1000, }); // Mock out the runInsideAngular operator so we don't have to deal with zone.js @@ -35,7 +35,6 @@ describe("foreground background derived state interactions", () => { let foreground: ForegroundDerivedState; let background: BackgroundDerivedState>; let parentState$: Subject; - let memoryStorage: FakeStorageService; const initialParent = "2020-01-01"; const ngZone = mock(); const portName = "testPort"; @@ -43,16 +42,9 @@ describe("foreground background derived state interactions", () => { beforeEach(() => { mockPorts(); parentState$ = new Subject(); - memoryStorage = new FakeStorageService(); - background = new BackgroundDerivedState( - parentState$, - deriveDefinition, - memoryStorage, - portName, - {}, - ); - foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone); + background = new BackgroundDerivedState(parentState$, deriveDefinition, portName, {}); + foreground = new ForegroundDerivedState(deriveDefinition, portName, ngZone); }); afterEach(() => { @@ -72,21 +64,13 @@ describe("foreground background derived state interactions", () => { }); it("should initialize a late-connected foreground", async () => { - const newForeground = new ForegroundDerivedState( - deriveDefinition, - memoryStorage, - portName, - ngZone, - ); - const backgroundEmissions = trackEmissions(background.state$); + const newForeground = new ForegroundDerivedState(deriveDefinition, portName, ngZone); + const backgroundTracker = new ObservableTracker(background.state$); parentState$.next(initialParent); - await awaitAsync(); + const foregroundTracker = new ObservableTracker(newForeground.state$); - const foregroundEmissions = trackEmissions(newForeground.state$); - await awaitAsync(10); - - expect(backgroundEmissions).toEqual([new Date(initialParent)]); - expect(foregroundEmissions).toEqual([new Date(initialParent)]); + expect(await backgroundTracker.expectEmission()).toEqual(new Date(initialParent)); + expect(await foregroundTracker.expectEmission()).toEqual(new Date(initialParent)); }); describe("forceValue", () => { diff --git a/apps/browser/src/platform/state/foreground-derived-state.provider.ts b/apps/browser/src/platform/state/foreground-derived-state.provider.ts index d9262e3b6e7..8b8d82b9143 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.provider.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.provider.ts @@ -1,11 +1,6 @@ import { NgZone } from "@angular/core"; import { Observable } from "rxjs"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; -import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; @@ -14,19 +9,18 @@ import { DerivedStateDependencies } from "@bitwarden/common/src/types/state"; import { ForegroundDerivedState } from "./foreground-derived-state"; export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider { - constructor( - storageServiceProvider: StorageServiceProvider, - private ngZone: NgZone, - ) { - super(storageServiceProvider); + constructor(private ngZone: NgZone) { + super(); } override buildDerivedState( _parentState$: Observable, deriveDefinition: DeriveDefinition, _dependencies: TDeps, - storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState { - const [cacheKey, storageService] = storageLocation; - return new ForegroundDerivedState(deriveDefinition, storageService, cacheKey, this.ngZone); + return new ForegroundDerivedState( + deriveDefinition, + deriveDefinition.buildCacheKey(), + this.ngZone, + ); } } diff --git a/apps/browser/src/platform/state/foreground-derived-state.spec.ts b/apps/browser/src/platform/state/foreground-derived-state.spec.ts index 2c29f39bc12..ee224540c13 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.spec.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.spec.ts @@ -1,11 +1,5 @@ -/** - * need to update test environment so structuredClone works appropriately - * @jest-environment ../../libs/shared/test.environment.ts - */ - import { NgZone } from "@angular/core"; -import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec"; -import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service"; +import { awaitAsync } from "@bitwarden/common/../spec"; import { mock } from "jest-mock-extended"; import { DeriveDefinition } from "@bitwarden/common/platform/state"; @@ -32,15 +26,12 @@ jest.mock("../browser/run-inside-angular.operator", () => { describe("ForegroundDerivedState", () => { let sut: ForegroundDerivedState; - let memoryStorage: FakeStorageService; const portName = "testPort"; const ngZone = mock(); beforeEach(() => { - memoryStorage = new FakeStorageService(); - memoryStorage.internalUpdateValuesRequireDeserialization(true); mockPorts(); - sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone); + sut = new ForegroundDerivedState(deriveDefinition, portName, ngZone); }); afterEach(() => { @@ -67,18 +58,4 @@ describe("ForegroundDerivedState", () => { expect(disconnectSpy).toHaveBeenCalled(); expect(sut["port"]).toBeNull(); }); - - it("should emit when the memory storage updates", async () => { - const dateString = "2020-01-01"; - const emissions = trackEmissions(sut.state$); - - await memoryStorage.save(deriveDefinition.storageKey, { - derived: true, - value: new Date(dateString), - }); - - await awaitAsync(); - - expect(emissions).toEqual([new Date(dateString)]); - }); }); diff --git a/apps/browser/src/platform/state/foreground-derived-state.ts b/apps/browser/src/platform/state/foreground-derived-state.ts index b9dda763dfd..6abe3638764 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.ts @@ -6,19 +6,14 @@ import { filter, firstValueFrom, map, - merge, of, share, switchMap, tap, timer, } from "rxjs"; -import { Jsonify, JsonObject } from "type-fest"; +import { Jsonify } from "type-fest"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; import { DerivedStateDependencies } from "@bitwarden/common/types/state"; @@ -27,41 +22,28 @@ import { fromChromeEvent } from "../browser/from-chrome-event"; import { runInsideAngular } from "../browser/run-inside-angular.operator"; export class ForegroundDerivedState implements DerivedState { - private storageKey: string; private port: chrome.runtime.Port; private backgroundResponses$: Observable; state$: Observable; constructor( private deriveDefinition: DeriveDefinition, - private memoryStorage: AbstractStorageService & ObservableStorageService, private portName: string, private ngZone: NgZone, ) { - this.storageKey = deriveDefinition.storageKey; - - const initialStorageGet$ = defer(() => { - return this.getStoredValue(); - }).pipe( - filter((s) => s.derived), - map((s) => s.value), - ); - - const latestStorage$ = this.memoryStorage.updates$.pipe( - filter((s) => s.key === this.storageKey), - switchMap(async (storageUpdate) => { - if (storageUpdate.updateType === "remove") { - return null; - } - - return await this.getStoredValue(); - }), - filter((s) => s.derived), - map((s) => s.value), - ); + const latestValueFromPort$ = (port: chrome.runtime.Port) => { + return fromChromeEvent(port.onMessage).pipe( + map(([message]) => message as DerivedStateMessage), + filter((message) => message.originator === "background" && message.action === "nextState"), + map((message) => { + const json = JSON.parse(message.data) as Jsonify; + return this.deriveDefinition.deserialize(json); + }), + ); + }; this.state$ = defer(() => of(this.initializePort())).pipe( - switchMap(() => merge(initialStorageGet$, latestStorage$)), + switchMap(() => latestValueFromPort$(this.port)), share({ connector: () => new ReplaySubject(1), resetOnRefCountZero: () => @@ -130,28 +112,4 @@ export class ForegroundDerivedState implements DerivedState { this.port = null; this.backgroundResponses$ = null; } - - protected async getStoredValue(): Promise<{ derived: boolean; value: TTo | null }> { - if (this.memoryStorage.valuesRequireDeserialization) { - const storedJson = await this.memoryStorage.get< - Jsonify<{ derived: true; value: JsonObject }> - >(this.storageKey); - - if (!storedJson?.derived) { - return { derived: false, value: null }; - } - - const value = this.deriveDefinition.deserialize(storedJson.value as any); - - return { derived: true, value }; - } else { - const stored = await this.memoryStorage.get<{ derived: true; value: TTo }>(this.storageKey); - - if (!stored?.derived) { - return { derived: false, value: null }; - } - - return { derived: true, value: stored.value }; - } - } } diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 5718542b016..2fb582d6932 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -36,6 +36,10 @@ import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { HeaderComponent } from "../platform/popup/header.component"; +import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../platform/popup/layout/popup-page.component"; +import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component"; import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component"; import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; @@ -108,6 +112,10 @@ import "../platform/popup/locales"; AccountComponent, ButtonModule, ExportScopeCalloutComponent, + PopupPageComponent, + PopupTabNavigationComponent, + PopupFooterComponent, + PopupHeaderComponent, ], declarations: [ ActionButtonsComponent, diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 73da4559410..80c75360872 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -68,7 +68,7 @@ img { border: none; } -a { +a:not(popup-page a, popup-tab-navigation a) { text-decoration: none; @include themify($themes) { @@ -171,7 +171,7 @@ cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb, } } -header:not(bit-callout header, bit-dialog header) { +header:not(bit-callout header, bit-dialog header, popup-page header) { height: 44px; display: flex; @@ -448,7 +448,7 @@ app-root { } } -main { +main:not(popup-page main) { position: absolute; top: 44px; bottom: 0; diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index ee842565d75..63ce45c9b76 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -2,6 +2,7 @@ import { DOCUMENT } from "@angular/common"; import { Inject, Injectable } from "@angular/core"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -15,6 +16,7 @@ export class InitService { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private stateService: StateServiceAbstraction, + private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, @Inject(DOCUMENT) private document: Document, @@ -24,6 +26,7 @@ export class InitService { return async () => { await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); + this.twoFactorService.init(); if (!BrowserPopupUtils.inPopup(window)) { window.document.body.classList.add("body-full"); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 38068d18495..bec278aeeb9 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -13,12 +13,10 @@ import { SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, INTRAPROCESS_MESSAGING_SUBJECT, + CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { - AuthRequestServiceAbstraction, - LoginStrategyServiceAbstraction, -} from "@bitwarden/auth/common"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; @@ -33,7 +31,6 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { @@ -49,6 +46,7 @@ import { UserNotificationSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ClientType } from "@bitwarden/common/enums"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -168,21 +166,11 @@ const safeProviders: SafeProvider[] = [ useClass: UnauthGuardService, deps: [AuthServiceAbstraction, Router], }), - safeProvider({ - provide: TwoFactorService, - useFactory: getBgService("twoFactorService"), - deps: [], - }), safeProvider({ provide: AuthServiceAbstraction, useFactory: getBgService("authService"), deps: [], }), - safeProvider({ - provide: LoginStrategyServiceAbstraction, - useFactory: getBgService("loginStrategyService"), - deps: [], - }), safeProvider({ provide: SsoLoginServiceAbstraction, useFactory: getBgService("ssoLoginService"), @@ -487,7 +475,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DerivedStateProvider, useClass: ForegroundDerivedStateProvider, - deps: [StorageServiceProvider, NgZone], + deps: [NgZone], }), safeProvider({ provide: AutofillSettingsServiceAbstraction, @@ -572,6 +560,10 @@ const safeProviders: SafeProvider[] = [ OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, ], }), + safeProvider({ + provide: CLIENT_TYPE, + useValue: ClientType.Browser, + }), ]; @NgModule({ diff --git a/apps/browser/src/popup/settings/about.component.html b/apps/browser/src/popup/settings/about.component.html index a4ad0ba801f..e68a664ba70 100644 --- a/apps/browser/src/popup/settings/about.component.html +++ b/apps/browser/src/popup/settings/about.component.html @@ -5,7 +5,7 @@
Bitwarden

© Bitwarden Inc. 2015-{{ year }}

-

{{ "version" | i18n }}: {{ version }}

+

{{ "version" | i18n }}: {{ version$ | async }}

{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }} diff --git a/apps/browser/src/popup/settings/about.component.ts b/apps/browser/src/popup/settings/about.component.ts index 61b5749b513..d7f98c1e7f7 100644 --- a/apps/browser/src/popup/settings/about.component.ts +++ b/apps/browser/src/popup/settings/about.component.ts @@ -1,14 +1,13 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { combineLatest, map } from "rxjs"; +import { Observable, combineLatest, defer, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule, DialogModule } from "@bitwarden/components"; -import { BrowserApi } from "../../platform/browser/browser-api"; - @Component({ templateUrl: "about.component.html", standalone: true, @@ -16,7 +15,7 @@ import { BrowserApi } from "../../platform/browser/browser-api"; }) export class AboutComponent { protected year = new Date().getFullYear(); - protected version = BrowserApi.getApplicationVersion(); + protected version$: Observable; protected data$ = combineLatest([ this.configService.serverConfig$, @@ -26,5 +25,8 @@ export class AboutComponent { constructor( private configService: ConfigService, private environmentService: EnvironmentService, - ) {} + private platformUtilsService: PlatformUtilsService, + ) { + this.version$ = defer(() => this.platformUtilsService.getApplicationVersion()); + } } diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/popup/settings/settings.component.ts index fa6c64fcc58..c7e5b7dc952 100644 --- a/apps/browser/src/popup/settings/settings.component.ts +++ b/apps/browser/src/popup/settings/settings.component.ts @@ -21,6 +21,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; @@ -86,6 +87,7 @@ export class SettingsComponent implements OnInit { private destroy$ = new Subject(); constructor( + private accountService: AccountService, private policyService: PolicyService, private formBuilder: FormBuilder, private platformUtilsService: PlatformUtilsService, @@ -434,8 +436,9 @@ export class SettingsComponent implements OnInit { type: "info", }); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; if (confirmed) { - this.messagingService.send("logout"); + this.messagingService.send("logout", { userId: userId }); } } diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts index 07c6408e8d2..fed5541f520 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.ts @@ -183,7 +183,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface return; } - const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( + const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( FeatureFlag.BrowserFilelessImport, ); const userAuthStatus = await this.authService.getAuthStatus(); diff --git a/apps/browser/src/tools/popup/services/browser-send-state.service.ts b/apps/browser/src/tools/popup/services/browser-send-state.service.ts index 52aeb01a925..11e71c9b20f 100644 --- a/apps/browser/src/tools/popup/services/browser-send-state.service.ts +++ b/apps/browser/src/tools/popup/services/browser-send-state.service.ts @@ -46,7 +46,9 @@ export class BrowserSendStateService { * the send component on the browser */ async setBrowserSendComponentState(value: BrowserSendComponentState): Promise { - await this.activeUserBrowserSendComponentState.update(() => value); + await this.activeUserBrowserSendComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } /** Get the active user's browser component state @@ -60,6 +62,8 @@ export class BrowserSendStateService { * @param { BrowserComponentState } value set the scroll position and search text for the send component on the browser */ async setBrowserSendTypeComponentState(value: BrowserComponentState): Promise { - await this.activeUserBrowserSendTypeComponentState.update(() => value); + await this.activeUserBrowserSendTypeComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } } diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index 4d2674fd703..d882dfd525d 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts @@ -292,6 +292,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy { const ciphers = await this.cipherService.getAllDecryptedForUrl( this.url, otherTypes.length > 0 ? otherTypes : null, + null, + false, ); this.loginCiphers = []; diff --git a/apps/browser/src/vault/services/vault-browser-state.service.ts b/apps/browser/src/vault/services/vault-browser-state.service.ts index a0d55a9d550..43a28928da5 100644 --- a/apps/browser/src/vault/services/vault-browser-state.service.ts +++ b/apps/browser/src/vault/services/vault-browser-state.service.ts @@ -52,7 +52,9 @@ export class VaultBrowserStateService { } async setBrowserGroupingsComponentState(value: BrowserGroupingsComponentState): Promise { - await this.activeUserVaultBrowserGroupingsComponentState.update(() => value); + await this.activeUserVaultBrowserGroupingsComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } async getBrowserVaultItemsComponentState(): Promise { @@ -60,6 +62,8 @@ export class VaultBrowserStateService { } async setBrowserVaultItemsComponentState(value: BrowserComponentState): Promise { - await this.activeUserVaultBrowserComponentState.update(() => value); + await this.activeUserVaultBrowserComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } } diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 505f1533ae8..e1bf2b7211c 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "moduleResolution": "node", "noImplicitAny": true, + "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "module": "ES2020", diff --git a/apps/cli/package.json b/apps/cli/package.json index b06caacfd48..d6c449de484 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -71,7 +71,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.16", + "tldts": "6.1.18", "zxcvbn": "4.4.2" } } diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 3606285c723..bd61727a6c7 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -231,7 +231,7 @@ export class LoginCommand { } } if (response.requiresTwoFactor) { - const twoFactorProviders = this.twoFactorService.getSupportedProviders(null); + const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null); if (twoFactorProviders.length === 0) { return Response.badRequest("No providers available for this client."); } @@ -272,7 +272,7 @@ export class LoginCommand { if ( twoFactorToken == null && - response.twoFactorProviders.size > 1 && + Object.keys(response.twoFactorProviders).length > 1 && selectedProvider.type === TwoFactorProviderType.Email ) { const emailReq = new TwoFactorEmailRequest(); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index ffe6c128b58..b3fb68fe638 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -314,7 +314,7 @@ export class Main { this.singleUserStateProvider, ); - this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider); + this.derivedStateProvider = new DefaultDerivedStateProvider(); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, @@ -344,6 +344,7 @@ export class Main { this.storageService, this.logService, new MigrationBuilderService(), + ClientType.Cli, ); this.stateService = new StateService( @@ -455,7 +456,11 @@ export class Main { this.stateProvider, ); - this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); + this.twoFactorService = new TwoFactorService( + this.i18nService, + this.platformUtilsService, + this.globalStateProvider, + ); this.passwordStrengthService = new PasswordStrengthService(); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index b888df80133..1e3a7fdfa59 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -15,6 +15,7 @@ import { SafeInjectionToken, STATE_FACTORY, INTRAPROCESS_MESSAGING_SUBJECT, + CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -25,6 +26,7 @@ import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/comm import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { ClientType } from "@bitwarden/common/enums"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -275,6 +277,10 @@ const safeProviders: SafeProvider[] = [ useClass: NativeMessagingManifestService, deps: [], }), + safeProvider({ + provide: CLIENT_TYPE, + useValue: ClientType.Desktop, + }), ]; @NgModule({ diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 86550b736f3..e82420a1f51 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1636,7 +1636,7 @@ "message": "Error enabling browser integration" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Une erreur s'est produite lors de l'action de l'intégration du navigateur." }, "browserIntegrationMasOnlyDesc": { "message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment." @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Succès" }, "troubleshooting": { "message": "Résolution de problèmes" diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 5c91fb4b944..838b3fc7c8e 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Sikeres" }, "troubleshooting": { "message": "Hibaelhárítás" diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index aa057f54ab6..e2e068362e3 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Izdevās" }, "troubleshooting": { "message": "Sarežģījumu novēršana" diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index b5f2a413d6f..f56572259ba 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Succes" }, "troubleshooting": { "message": "Probleemoplossing" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 9837be29e3d..aad13e06ef1 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "成功" }, "troubleshooting": { "message": "故障排除" diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index da4c14b4aa6..d11fceeacc9 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -6,6 +6,7 @@ import { Subject, firstValueFrom } from "rxjs"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; +import { ClientType } from "@bitwarden/common/enums"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -157,7 +158,7 @@ export class Main { activeUserStateProvider, singleUserStateProvider, globalStateProvider, - new DefaultDerivedStateProvider(storageServiceProvider), + new DefaultDerivedStateProvider(), ); this.environmentService = new DefaultEnvironmentService(stateProvider, accountService); @@ -190,6 +191,7 @@ export class Main { this.storageService, this.logService, new MigrationBuilderService(), + ClientType.Desktop, ); // TODO: this state service will have access to on disk storage, but not in memory storage. diff --git a/apps/desktop/src/platform/services/desktop-settings.service.ts b/apps/desktop/src/platform/services/desktop-settings.service.ts index d967e5fb1d9..09ddad07c1b 100644 --- a/apps/desktop/src/platform/services/desktop-settings.service.ts +++ b/apps/desktop/src/platform/services/desktop-settings.service.ts @@ -164,7 +164,7 @@ export class DesktopSettingsService { /** * Sets the setting for whether or not the application should be shown in the dock. - * @param value `true` if the application should should in the dock, `false` if it should not. + * @param value `true` if the application should show in the dock, `false` if it should not. */ async setAlwaysShowDock(value: boolean) { await this.alwaysShowDockState.update(() => value); diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index b1a84c22f35..7de0c98cd5a 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -58,7 +58,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); constructor( diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index dea6f4999b2..b18effac862 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -194,7 +194,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { }), ); }), - shareReplay({ refCount: false }), + shareReplay({ refCount: true, bufferSize: 1 }), ); restrictGroupAccess$ = combineLatest([ diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index f1af9506505..a67bea39c0a 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -218,7 +218,6 @@ export class MemberDialogComponent implements OnDestroy { groups: groups$, flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ), }) .pipe(takeUntil(this.destroy$)) @@ -620,7 +619,7 @@ export class MemberDialogComponent implements OnDestroy { } function mapCollectionToAccessItemView( - collection: CollectionView, + collection: CollectionAdminView, organization: Organization, flexibleCollectionsV1Enabled: boolean, accessSelection?: CollectionAccessSelectionView, @@ -632,7 +631,8 @@ function mapCollectionToAccessItemView( labelName: collection.name, listName: collection.name, readonly: - group !== undefined || !collection.canEdit(organization, flexibleCollectionsV1Enabled), + group !== undefined || + !collection.canEditUserAccess(organization, flexibleCollectionsV1Enabled), readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined, viaGroupName: group?.name, }; diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 1ce05f7a302..d8091e46aef 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -44,12 +44,10 @@ export class AccountComponent { protected flexibleCollectionsMigrationEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsMigration, - false, ); flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ); // FormGroup validators taken from server Organization domain object diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index abf1d249e16..80d77968f2d 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -10,6 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { DialogService } from "@bitwarden/components"; import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component"; import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component"; @@ -22,6 +23,7 @@ import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../.. export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { tabbedHeader = false; constructor( + dialogService: DialogService, apiService: ApiService, modalService: ModalService, messagingService: MessagingService, @@ -31,6 +33,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + dialogService, apiService, modalService, messagingService, diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts index 94c62081154..dc5f9337247 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts @@ -90,7 +90,7 @@ export class UserKeyRotationService { request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey); request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey); - if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) { + if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) { await this.apiService.postUserKeyUpdate(request); } else { await this.rotateUserKeyAndEncryptedDataLegacy(request); diff --git a/apps/web/src/app/auth/settings/two-factor-authenticator.component.html b/apps/web/src/app/auth/settings/two-factor-authenticator.component.html index 33bf4fb1304..e17714cca79 100644 --- a/apps/web/src/app/auth/settings/two-factor-authenticator.component.html +++ b/apps/web/src/app/auth/settings/two-factor-authenticator.component.html @@ -15,13 +15,6 @@

- -