From 3b2a2e3129018813fd51c939060c3fb4b6225405 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:45:02 -0500 Subject: [PATCH 01/55] refactor `dialogRef` to be optional as the service can be used from the context of a dialog and without it (#16581) --- apps/web/src/app/core/core.module.ts | 7 +++++++ .../vault-item-dialog.component.ts | 17 ++++++++++++++--- .../vault/individual-vault/view.component.ts | 7 +------ .../web-premium-upgrade-prompt.service.ts | 6 +++--- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 06c31a0bfd4..29b84ddc382 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -98,6 +98,7 @@ import { DefaultThemeStateService, ThemeStateService, } from "@bitwarden/common/platform/theming/theme-state.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { @@ -109,6 +110,7 @@ import { LockComponentService } from "@bitwarden/key-management-ui"; import { SerializedMemoryStorageService } from "@bitwarden/storage-core"; import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault"; import { WebOrganizationInviteService } from "@bitwarden/web-vault/app/auth/core/services/organization-invite/web-organization-invite.service"; +import { WebVaultPremiumUpgradePromptService } from "@bitwarden/web-vault/app/vault/services/web-premium-upgrade-prompt.service"; import { flagEnabled } from "../../utils/flags"; import { @@ -403,6 +405,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultDeviceManagementComponentService, deps: [], }), + safeProvider({ + provide: PremiumUpgradePromptService, + useClass: WebVaultPremiumUpgradePromptService, + deps: [DialogService, Router], + }), ]; @NgModule({ diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 42ad477ff51..b48db2bba91 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -1,7 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { CommonModule } from "@angular/common"; -import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { + Component, + ElementRef, + forwardRef, + Inject, + OnDestroy, + OnInit, + ViewChild, +} from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; import { firstValueFrom, Subject, switchMap } from "rxjs"; @@ -56,10 +64,10 @@ import { } from "@bitwarden/vault"; import { SharedModule } from "../../../shared/shared.module"; -import { WebVaultPremiumUpgradePromptService } from "../../../vault/services/web-premium-upgrade-prompt.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service"; +import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service"; export type VaultItemDialogMode = "view" | "form"; @@ -136,7 +144,10 @@ export type VaultItemDialogResult = UnionOfValues; PremiumBadgeComponent, ], providers: [ - { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, + { + provide: PremiumUpgradePromptService, + useClass: forwardRef(() => WebVaultPremiumUpgradePromptService), + }, { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, { provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }, RoutedVaultFilterService, diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index ea0b66f12d0..6de29f8e328 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -15,7 +15,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { CollectionId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -33,7 +32,6 @@ import { import { CipherViewComponent } from "@bitwarden/vault"; import { SharedModule } from "../../shared/shared.module"; -import { WebVaultPremiumUpgradePromptService } from "../../vault/services/web-premium-upgrade-prompt.service"; export interface ViewCipherDialogParams { cipher: CipherView; @@ -75,10 +73,7 @@ export interface ViewCipherDialogCloseResult { selector: "app-vault-view", templateUrl: "view.component.html", imports: [CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, SharedModule], - providers: [ - { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, - { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, - ], + providers: [{ provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }], }) export class ViewComponent implements OnInit { cipher: CipherView; diff --git a/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts index 7dfd1146469..87fcdc345d8 100644 --- a/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts +++ b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@angular/core"; +import { Injectable, Optional } from "@angular/core"; import { Router } from "@angular/router"; import { Subject } from "rxjs"; @@ -16,7 +16,7 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt constructor( private dialogService: DialogService, private router: Router, - private dialog: DialogRef, + @Optional() private dialog?: DialogRef, ) {} /** @@ -53,7 +53,7 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt if (route) { await this.router.navigate(route); } - if (confirmed) { + if (confirmed && this.dialog) { this.dialog.close(VaultItemDialogResult.PremiumUpgrade); } } From e3a5111a35d5fe37b5ceb7423c590d863334e78e Mon Sep 17 00:00:00 2001 From: Vicki League Date: Thu, 25 Sep 2025 14:52:55 -0400 Subject: [PATCH 02/55] [CL-849] Update and consolidate logo svgs (#16390) --- ...tension-anon-layout-wrapper.component.html | 9 +++------ ...extension-anon-layout-wrapper.component.ts | 4 ++-- libs/assets/src/svg/svgs/admin-console.ts | 12 ++++++++++- libs/assets/src/svg/svgs/bitwarden-icon.ts | 20 +++++++++---------- .../src/svg/svgs/business-unit-portal.ts | 12 ++++++++++- .../svg/svgs/extension-bitwarden-logo.icon.ts | 19 ------------------ libs/assets/src/svg/svgs/index.ts | 1 - libs/assets/src/svg/svgs/password-manager.ts | 12 ++++++++++- libs/assets/src/svg/svgs/provider-portal.ts | 12 ++++++++++- libs/assets/src/svg/svgs/secrets-manager.ts | 12 ++++++++++- libs/assets/src/svg/svgs/shield.ts | 13 ++++++++++-- .../src/navigation/nav-logo.component.html | 3 ++- 12 files changed, 83 insertions(+), 46 deletions(-) delete mode 100644 libs/assets/src/svg/svgs/extension-bitwarden-logo.icon.ts diff --git a/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html index 4c394317d14..d389fd8d783 100644 --- a/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html +++ b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -5,12 +5,9 @@ [showBackButton]="showBackButton" [pageTitle]="''" > - +
+ +
diff --git a/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 3b84eac2217..c1694d80668 100644 --- a/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -5,7 +5,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router"; import { Subject, filter, switchMap, takeUntil, tap } from "rxjs"; -import { ExtensionBitwardenLogo, Icon } from "@bitwarden/assets/svg"; +import { BitwardenLogo, Icon } from "@bitwarden/assets/svg"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { IconModule, @@ -62,7 +62,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { protected hideCardWrapper: boolean = false; protected theme: string; - protected logo = ExtensionBitwardenLogo; + protected logo = BitwardenLogo; constructor( private router: Router, diff --git a/libs/assets/src/svg/svgs/admin-console.ts b/libs/assets/src/svg/svgs/admin-console.ts index 596822dde57..83c8cf9f0e1 100644 --- a/libs/assets/src/svg/svgs/admin-console.ts +++ b/libs/assets/src/svg/svgs/admin-console.ts @@ -1,7 +1,17 @@ import { svgIcon } from "../icon-service"; const AdminConsoleLogo = svgIcon` - + + + + + + + + + + + `; export default AdminConsoleLogo; diff --git a/libs/assets/src/svg/svgs/bitwarden-icon.ts b/libs/assets/src/svg/svgs/bitwarden-icon.ts index 710d20937bd..2f88b38043c 100644 --- a/libs/assets/src/svg/svgs/bitwarden-icon.ts +++ b/libs/assets/src/svg/svgs/bitwarden-icon.ts @@ -3,18 +3,18 @@ import { svgIcon } from "../icon-service"; export const BitwardenIcon = svgIcon` - - - - - - - + + + + + + + - - - + + + `; diff --git a/libs/assets/src/svg/svgs/business-unit-portal.ts b/libs/assets/src/svg/svgs/business-unit-portal.ts index bae5ebada6a..db3a6b8ef4f 100644 --- a/libs/assets/src/svg/svgs/business-unit-portal.ts +++ b/libs/assets/src/svg/svgs/business-unit-portal.ts @@ -1,7 +1,17 @@ import { svgIcon } from "../icon-service"; const BusinessUnitPortalLogo = svgIcon` - + + + + + + + + + + + `; export default BusinessUnitPortalLogo; diff --git a/libs/assets/src/svg/svgs/extension-bitwarden-logo.icon.ts b/libs/assets/src/svg/svgs/extension-bitwarden-logo.icon.ts deleted file mode 100644 index 42a74e006bc..00000000000 --- a/libs/assets/src/svg/svgs/extension-bitwarden-logo.icon.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { svgIcon } from "../icon-service"; - -export const ExtensionBitwardenLogo = svgIcon` - - Bitwarden - - -`; diff --git a/libs/assets/src/svg/svgs/index.ts b/libs/assets/src/svg/svgs/index.ts index 10f2f7cd8d4..ab4f2c23f13 100644 --- a/libs/assets/src/svg/svgs/index.ts +++ b/libs/assets/src/svg/svgs/index.ts @@ -12,7 +12,6 @@ export * from "./deactivated-org"; export * from "./devices.icon"; export * from "./domain.icon"; export * from "./empty-trash"; -export * from "./extension-bitwarden-logo.icon"; export * from "./gear"; export * from "./generator"; export * from "./item-types"; diff --git a/libs/assets/src/svg/svgs/password-manager.ts b/libs/assets/src/svg/svgs/password-manager.ts index 5c8ef852025..17b6f148be3 100644 --- a/libs/assets/src/svg/svgs/password-manager.ts +++ b/libs/assets/src/svg/svgs/password-manager.ts @@ -1,7 +1,17 @@ import { svgIcon } from "../icon-service"; const PasswordManagerLogo = svgIcon` - + + + + + + + + + + `; export default PasswordManagerLogo; diff --git a/libs/assets/src/svg/svgs/provider-portal.ts b/libs/assets/src/svg/svgs/provider-portal.ts index 54e4b219385..51c04e1553b 100644 --- a/libs/assets/src/svg/svgs/provider-portal.ts +++ b/libs/assets/src/svg/svgs/provider-portal.ts @@ -1,7 +1,17 @@ import { svgIcon } from "../icon-service"; const ProviderPortalLogo = svgIcon` - + + + + + + + + + + + `; export default ProviderPortalLogo; diff --git a/libs/assets/src/svg/svgs/secrets-manager.ts b/libs/assets/src/svg/svgs/secrets-manager.ts index 3a5095b95bd..27589e7e2f9 100644 --- a/libs/assets/src/svg/svgs/secrets-manager.ts +++ b/libs/assets/src/svg/svgs/secrets-manager.ts @@ -1,7 +1,17 @@ import { svgIcon } from "../icon-service"; const SecretsManagerLogo = svgIcon` - + + + + + + + + + + + `; export default SecretsManagerLogo; diff --git a/libs/assets/src/svg/svgs/shield.ts b/libs/assets/src/svg/svgs/shield.ts index b0c781bd00d..eaf9780773e 100644 --- a/libs/assets/src/svg/svgs/shield.ts +++ b/libs/assets/src/svg/svgs/shield.ts @@ -4,13 +4,22 @@ import { svgIcon } from "../icon-service"; * Shield logo with extra space in the viewbox. */ const AnonLayoutBitwardenShield = svgIcon` - + `; const BitwardenShield = svgIcon` - + + + + + + + + + + `; export { AnonLayoutBitwardenShield, BitwardenShield }; diff --git a/libs/components/src/navigation/nav-logo.component.html b/libs/components/src/navigation/nav-logo.component.html index 89507503a41..391e62ec8fd 100644 --- a/libs/components/src/navigation/nav-logo.component.html +++ b/libs/components/src/navigation/nav-logo.component.html @@ -1,7 +1,7 @@
@@ -12,6 +12,7 @@ [ngClass]="{ '!tw-h-[55px] [&_svg]:!tw-w-[26px] [&_svg]:tw-inset-y-[theme(spacing.3)]': !sideNavService.open, + 'tw-w-56': sideNavService.open, }" [attr.aria-label]="label()" [title]="label()" From bed5ea17b7096d60c80a6302d60482299952f8e6 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:45:33 -0700 Subject: [PATCH 03/55] check for undefined parent and node (#16584) --- .../admin-console/organizations/collections/vault.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index edd4ea3f9dc..64aa6936468 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -1132,7 +1132,7 @@ export class vNextVaultComponent implements OnInit, OnDestroy { const selectedCollection = await firstValueFrom(this.selectedCollection$); if (selectedCollection?.node.id === collection.id) { void this.router.navigate([], { - queryParams: { collectionId: selectedCollection.parent.node.id ?? null }, + queryParams: { collectionId: selectedCollection?.parent?.node.id ?? null }, queryParamsHandling: "merge", replaceUrl: true, }); From b56c9e6c9ae25bcc376224cc27770c0f01e79226 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Thu, 25 Sep 2025 16:57:31 -0400 Subject: [PATCH 04/55] [PM-26078] Create admin UI for Autotype Desktop Default Setting Policy (#16573) * PM-26078 add component and copy * add enum * register oss policy * export policy definition * update naming to match policy --- .../autotype-policy.component.html | 4 +++ .../autotype-policy.component.ts | 25 +++++++++++++++++++ .../policies/policy-edit-definitions/index.ts | 1 + .../policies/policy-edit-register.ts | 2 ++ apps/web/src/locales/en/messages.json | 7 ++++++ .../admin-console/enums/policy-type.enum.ts | 1 + 6 files changed, 40 insertions(+) create mode 100644 apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.html create mode 100644 apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.ts diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.html new file mode 100644 index 00000000000..f110e7d34cd --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.html @@ -0,0 +1,4 @@ + + + {{ "turnOn" | i18n }} + diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.ts new file mode 100644 index 00000000000..ce62a7ff5a3 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; + +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { SharedModule } from "../../../../shared"; +import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component"; + +export class DesktopAutotypeDefaultSettingPolicy extends BasePolicyEditDefinition { + name = "desktopAutotypePolicy"; + description = "desktopAutotypePolicyDesc"; + type = PolicyType.AutotypeDefaultSetting; + component = DesktopAutotypeDefaultSettingPolicyComponent; + + display$(organization: Organization, configService: ConfigService) { + return configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype); + } +} +@Component({ + templateUrl: "autotype-policy.component.html", + imports: [SharedModule], +}) +export class DesktopAutotypeDefaultSettingPolicyComponent extends BasePolicyEditComponent {} diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts index 13f29ab68f7..bb2c40b7a76 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts @@ -1,4 +1,5 @@ export { DisableSendPolicy } from "./disable-send.component"; +export { DesktopAutotypeDefaultSettingPolicy } from "./autotype-policy.component"; export { MasterPasswordPolicy } from "./master-password.component"; export { OrganizationDataOwnershipPolicy } from "./organization-data-ownership.component"; export { PasswordGeneratorPolicy } from "./password-generator.component"; diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts index 3a4ba9a710f..5e63ba1358a 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts @@ -1,5 +1,6 @@ import { BasePolicyEditDefinition } from "./base-policy-edit.component"; import { + DesktopAutotypeDefaultSettingPolicy, DisableSendPolicy, MasterPasswordPolicy, OrganizationDataOwnershipPolicy, @@ -31,4 +32,5 @@ export const ossPolicyEditRegister: BasePolicyEditDefinition[] = [ new DisableSendPolicy(), new SendOptionsPolicy(), new RestrictedItemTypesPolicy(), + new DesktopAutotypeDefaultSettingPolicy(), ]; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 11d2d8e3dd8..1646235d8cb 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5561,6 +5561,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, diff --git a/libs/common/src/admin-console/enums/policy-type.enum.ts b/libs/common/src/admin-console/enums/policy-type.enum.ts index 91f3a8229f8..a4a860a2f3f 100644 --- a/libs/common/src/admin-console/enums/policy-type.enum.ts +++ b/libs/common/src/admin-console/enums/policy-type.enum.ts @@ -17,4 +17,5 @@ export enum PolicyType { FreeFamiliesSponsorshipPolicy = 13, // Disables free families plan for organization RemoveUnlockWithPin = 14, // Do not allow members to unlock their account with a PIN. RestrictedItemTypes = 15, // Restricts item types that can be created within an organization + AutotypeDefaultSetting = 17, // Sets the default autotype setting for desktop app } From 2f34c6b9c6528344341ea83694114364f1090600 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Thu, 25 Sep 2025 18:09:53 -0400 Subject: [PATCH 05/55] [PM-26053] Create Autotype Desktop Default Setting Policy for use within the desktop autotype service (#16537) * add policy type enum * desktop autotype service which emits an observable * add desktop autotype default setting policy to the app constructor * update service module to include DesktopAutotypeDefaultSettingPolicy * flag the service * add tests * address comments, switch to null remove false, update tests --- apps/desktop/src/app/app.component.ts | 2 + .../src/app/services/services.module.ts | 11 +- .../desktop-autotype-policy.service.spec.ts | 166 ++++++++++++++++++ .../desktop-autotype-policy.service.ts | 60 +++++++ 4 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/autofill/services/desktop-autotype-policy.service.spec.ts create mode 100644 apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 1e7ef8e0000..1c2d3aa464d 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -77,6 +77,7 @@ import { KeyService, BiometricStateService } from "@bitwarden/key-management"; import { AddEditFolderDialogComponent, AddEditFolderDialogResult } from "@bitwarden/vault"; import { DeleteAccountComponent } from "../auth/delete-account.component"; +import { DesktopAutotypeDefaultSettingPolicy } from "../autofill/services/desktop-autotype-policy.service"; import { PremiumComponent } from "../billing/app/accounts/premium.component"; import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater"; @@ -177,6 +178,7 @@ export class AppComponent implements OnInit, OnDestroy { private readonly documentLangSetter: DocumentLangSetter, private restrictedItemTypesService: RestrictedItemTypesService, private readonly tokenService: TokenService, + private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 094dea2e215..ea16f36402c 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -37,7 +37,10 @@ import { } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { + PolicyService as PolicyServiceAbstraction, + InternalPolicyService, +} from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService, AccountService as AccountServiceAbstraction, @@ -112,6 +115,7 @@ import { DesktopLoginComponentService } from "../../auth/login/desktop-login-com import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; +import { DesktopAutotypeDefaultSettingPolicy } from "../../autofill/services/desktop-autotype-policy.service"; import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service"; import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service"; import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service"; @@ -466,6 +470,11 @@ const safeProviders: SafeProvider[] = [ BillingAccountProfileStateService, ], }), + safeProvider({ + provide: DesktopAutotypeDefaultSettingPolicy, + useClass: DesktopAutotypeDefaultSettingPolicy, + deps: [AccountServiceAbstraction, AuthServiceAbstraction, InternalPolicyService, ConfigService], + }), ]; @NgModule({ diff --git a/apps/desktop/src/autofill/services/desktop-autotype-policy.service.spec.ts b/apps/desktop/src/autofill/services/desktop-autotype-policy.service.spec.ts new file mode 100644 index 00000000000..7fb30333e28 --- /dev/null +++ b/apps/desktop/src/autofill/services/desktop-autotype-policy.service.spec.ts @@ -0,0 +1,166 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, firstValueFrom, take, timeout, TimeoutError } from "rxjs"; + +import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { Account, UserId } from "@bitwarden/common/platform/models/domain/account"; + +import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service"; + +describe("DesktopAutotypeDefaultSettingPolicy", () => { + let service: DesktopAutotypeDefaultSettingPolicy; + let accountService: MockProxy; + let authService: MockProxy; + let policyService: MockProxy; + let configService: MockProxy; + + let mockAccountSubject: BehaviorSubject<{ id: UserId } | null>; + let mockFeatureFlagSubject: BehaviorSubject; + let mockAuthStatusSubject: BehaviorSubject; + let mockPolicyAppliesSubject: BehaviorSubject; + + const mockUserId = "user-123" as UserId; + + beforeEach(() => { + mockAccountSubject = new BehaviorSubject({ + id: mockUserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); + mockFeatureFlagSubject = new BehaviorSubject(true); + mockAuthStatusSubject = new BehaviorSubject( + AuthenticationStatus.Unlocked, + ); + mockPolicyAppliesSubject = new BehaviorSubject(false); + + accountService = mock(); + authService = mock(); + policyService = mock(); + configService = mock(); + + accountService.activeAccount$ = mockAccountSubject.asObservable(); + configService.getFeatureFlag$ = jest + .fn() + .mockReturnValue(mockFeatureFlagSubject.asObservable()); + authService.authStatusFor$ = jest + .fn() + .mockImplementation((_: UserId) => mockAuthStatusSubject.asObservable()); + policyService.policyAppliesToUser$ = jest + .fn() + .mockReturnValue(mockPolicyAppliesSubject.asObservable()); + + TestBed.configureTestingModule({ + providers: [ + DesktopAutotypeDefaultSettingPolicy, + { provide: AccountService, useValue: accountService }, + { provide: AuthService, useValue: authService }, + { provide: InternalPolicyService, useValue: policyService }, + { provide: ConfigService, useValue: configService }, + ], + }); + + service = TestBed.inject(DesktopAutotypeDefaultSettingPolicy); + }); + + afterEach(() => { + jest.clearAllMocks(); + mockAccountSubject.complete(); + mockFeatureFlagSubject.complete(); + mockAuthStatusSubject.complete(); + mockPolicyAppliesSubject.complete(); + }); + + describe("autotypeDefaultSetting$", () => { + it("should emit null when feature flag is disabled", async () => { + mockFeatureFlagSubject.next(false); + const result = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(result).toBeNull(); + }); + + it("should not emit when no active account", async () => { + mockAccountSubject.next(null); + await expect( + firstValueFrom(service.autotypeDefaultSetting$.pipe(timeout({ first: 30 }))), + ).rejects.toBeInstanceOf(TimeoutError); + }); + + it("should emit null when user is not unlocked", async () => { + mockAuthStatusSubject.next(AuthenticationStatus.Locked); + const result = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(result).toBeNull(); + }); + + it("should emit null when no autotype policy exists", async () => { + mockPolicyAppliesSubject.next(false); + const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(policy).toBeNull(); + }); + + it("should emit true when autotype policy is enabled", async () => { + mockPolicyAppliesSubject.next(true); + const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(policyStatus).toBe(true); + }); + + it("should emit false when autotype policy is disabled", async () => { + mockPolicyAppliesSubject.next(false); + const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(policyStatus).toBeNull(); + }); + + it("should emit null when autotype policy does not apply", async () => { + mockPolicyAppliesSubject.next(false); + const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(policy).toBeNull(); + }); + + it("should react to authentication status changes", async () => { + // Expect one emission when unlocked + mockAuthStatusSubject.next(AuthenticationStatus.Unlocked); + const first = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(first).toBeNull(); + + // Expect null emission when locked + mockAuthStatusSubject.next(AuthenticationStatus.Locked); + const lockedResult = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(lockedResult).toBeNull(); + }); + + it("should react to account changes", async () => { + const newUserId = "user-456" as UserId; + + // First value for original user + const firstValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(firstValue).toBeNull(); + + // Change account and expect a new emission + mockAccountSubject.next({ + id: newUserId, + }); + const secondValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(secondValue).toBeNull(); + + // Verify the auth lookup was switched to the new user + expect(authService.authStatusFor$).toHaveBeenCalledWith(newUserId); + }); + + it("should react to policy changes", async () => { + mockPolicyAppliesSubject.next(false); + const nullValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(nullValue).toBeNull(); + + mockPolicyAppliesSubject.next(true); + const trueValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(trueValue).toBe(true); + + mockPolicyAppliesSubject.next(false); + const nullValueAgain = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(nullValueAgain).toBeNull(); + }); + }); +}); diff --git a/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts b/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts new file mode 100644 index 00000000000..76ffc090600 --- /dev/null +++ b/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from "@angular/core"; +import { Observable, of } from "rxjs"; +import { distinctUntilChanged, filter, map, shareReplay, switchMap } from "rxjs/operators"; + +import { InternalPolicyService } 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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +@Injectable({ providedIn: "root" }) +export class DesktopAutotypeDefaultSettingPolicy { + constructor( + private readonly accountService: AccountService, + private readonly authService: AuthService, + private readonly policyService: InternalPolicyService, + private readonly configService: ConfigService, + ) {} + + /** + * Emits the autotype policy enabled status (true | false | null) when account is unlocked and WindowsDesktopAutotype is enabled. + * - true: autotype policy exists and is enabled + * - null: no autotype policy exists for the user's organization + */ + readonly autotypeDefaultSetting$: Observable = this.configService + .getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype) + .pipe( + switchMap((autotypeFeatureEnabled) => { + if (!autotypeFeatureEnabled) { + return of(null); + } + + return this.accountService.activeAccount$.pipe( + filter((account) => account != null), + getUserId, + distinctUntilChanged(), + switchMap((userId) => { + const isUnlocked$ = this.authService.authStatusFor$(userId).pipe( + map((status) => status === AuthenticationStatus.Unlocked), + distinctUntilChanged(), + ); + + const policy$ = this.policyService + .policyAppliesToUser$(PolicyType.AutotypeDefaultSetting, userId) + .pipe( + map((appliesToUser) => (appliesToUser ? true : null)), + distinctUntilChanged(), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + return isUnlocked$.pipe(switchMap((unlocked) => (unlocked ? policy$ : of(null)))); + }), + ); + }), + shareReplay({ bufferSize: 1, refCount: true }), + ); +} From 1c823ed9f68e40540c6d9368be1de6e5e6e90193 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:03:08 -0400 Subject: [PATCH 06/55] Enhance accessibility by adding tabindex attributes to sortable table headers and options button in app-table-row-scrollable.component.html (#16597) --- .../app-table-row-scrollable.component.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html index 720c3ec04d0..3d3a9baa6e8 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html @@ -3,13 +3,13 @@ - {{ "application" | i18n }} - + {{ "application" | i18n }} + {{ "atRiskPasswords" | i18n }} - {{ "totalPasswords" | i18n }} - {{ "atRiskMembers" | i18n }} - {{ "totalMembers" | i18n }} + {{ "totalPasswords" | i18n }} + {{ "atRiskMembers" | i18n }} + {{ "totalMembers" | i18n }} From 4a9183adc51ea7535a4c870c60590f9197b293c2 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:21:28 +0200 Subject: [PATCH 07/55] Allow autofilling iframes like samsclub.com (#16560) * Allow autofilling iframes like samsclub.com * Add back original checks * Remove unused mock --- .../insert-autofill-content.service.spec.ts | 4 +++- apps/browser/src/autofill/utils/index.ts | 23 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) 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 e36d6811ecb..9edcdbb3a95 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 @@ -153,7 +153,9 @@ describe("InsertAutofillContentService", () => { it("returns early if the script is filling within a sand boxed iframe", async () => { Object.defineProperty(globalThis, "frameElement", { - value: { hasAttribute: jest.fn(() => true) }, + value: { + getAttribute: jest.fn(() => ""), + }, writable: true, }); jest.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill"); diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 0e102dcfd99..a3d61c7f0b2 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -499,11 +499,24 @@ export function isInvalidResponseStatusCode(statusCode: number) { * Determines if the current context is within a sandboxed iframe. */ export function currentlyInSandboxedIframe(): boolean { - return ( - String(self.origin).toLowerCase() === "null" || - globalThis.frameElement?.hasAttribute("sandbox") || - globalThis.location.hostname === "" - ); + if (String(self.origin).toLowerCase() === "null" || globalThis.location.hostname === "") { + return true; + } + + const sandbox = globalThis.frameElement?.getAttribute?.("sandbox"); + + // No frameElement or sandbox attribute means not sandboxed + if (sandbox === null || sandbox === undefined) { + return false; + } + + // An empty string means fully sandboxed + if (sandbox === "") { + return true; + } + + const tokens = new Set(sandbox.toLowerCase().split(" ")); + return !["allow-scripts", "allow-same-origin"].every((token) => tokens.has(token)); } /** From fd98dda0a5b5020e43ba3c0d80753e769b862afe Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:17:41 +0200 Subject: [PATCH 08/55] Autofill form that has the focus (#16499) --- .../background/abstractions/overlay.background.ts | 1 + .../src/autofill/background/overlay.background.ts | 2 ++ .../autofill/services/abstractions/autofill.service.ts | 1 + .../services/autofill-overlay-content.service.ts | 1 + apps/browser/src/autofill/services/autofill.service.ts | 10 +++++++++- 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 75f2659c9df..6067d563db2 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -48,6 +48,7 @@ export type FocusedFieldData = { frameId?: number; accountCreationFieldType?: string; showPasskeys?: boolean; + focusedFieldForm?: string; }; export type InlineMenuElementPosition = { diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index a2eb6eb7e90..5da00cfaff2 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1175,6 +1175,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { pageDetails, fillNewPassword: true, allowTotpAutofill: true, + focusedFieldForm: this.focusedFieldData?.focusedFieldForm, }); if (totpCode) { @@ -1859,6 +1860,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { pageDetails, fillNewPassword: true, allowTotpAutofill: false, + focusedFieldForm: this.focusedFieldData?.focusedFieldForm, }); globalThis.setTimeout(async () => { diff --git a/apps/browser/src/autofill/services/abstractions/autofill.service.ts b/apps/browser/src/autofill/services/abstractions/autofill.service.ts index 5eb8ee99eb5..09e22e278be 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill.service.ts @@ -30,6 +30,7 @@ export interface AutoFillOptions { allowUntrustedIframe?: boolean; allowTotpAutofill?: boolean; autoSubmitLogin?: boolean; + focusedFieldForm?: string; } export interface FormData { 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 07c97a5a344..656516d1119 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -974,6 +974,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ inlineMenuFillType: autofillFieldData?.inlineMenuFillType, showPasskeys: !!autofillFieldData?.showPasskeys, accountCreationFieldType: autofillFieldData?.accountCreationFieldType, + focusedFieldForm: autofillFieldData?.form, }; const allFields = this.formFieldElements; diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 512690929cc..89c3e2ee175 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -434,7 +434,15 @@ export default class AutofillService implements AutofillServiceInterface { return; } - const fillScript = await this.generateFillScript(pd.details, { + // If we have a focused form, filter the page details to only include fields from that form + const details = options.focusedFieldForm + ? { + ...pd.details, + fields: pd.details.fields.filter((f) => f.form === options.focusedFieldForm), + } + : pd.details; + + const fillScript = await this.generateFillScript(details, { skipUsernameOnlyFill: options.skipUsernameOnlyFill || false, onlyEmptyFields: options.onlyEmptyFields || false, fillNewPassword: options.fillNewPassword || false, From 610bc5b7c26ae0ceace20b6502153d482d86aaa1 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:05:10 +0200 Subject: [PATCH 09/55] Autosync the updated translations (#16608) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 27 +++++++ apps/browser/src/_locales/az/messages.json | 27 +++++++ apps/browser/src/_locales/be/messages.json | 27 +++++++ apps/browser/src/_locales/bg/messages.json | 27 +++++++ apps/browser/src/_locales/bn/messages.json | 27 +++++++ apps/browser/src/_locales/bs/messages.json | 27 +++++++ apps/browser/src/_locales/ca/messages.json | 27 +++++++ apps/browser/src/_locales/cs/messages.json | 27 +++++++ apps/browser/src/_locales/cy/messages.json | 27 +++++++ apps/browser/src/_locales/da/messages.json | 27 +++++++ apps/browser/src/_locales/de/messages.json | 35 +++++++- apps/browser/src/_locales/el/messages.json | 27 +++++++ apps/browser/src/_locales/en_GB/messages.json | 27 +++++++ apps/browser/src/_locales/en_IN/messages.json | 27 +++++++ apps/browser/src/_locales/es/messages.json | 27 +++++++ apps/browser/src/_locales/et/messages.json | 27 +++++++ apps/browser/src/_locales/eu/messages.json | 27 +++++++ apps/browser/src/_locales/fa/messages.json | 27 +++++++ apps/browser/src/_locales/fi/messages.json | 27 +++++++ apps/browser/src/_locales/fil/messages.json | 27 +++++++ apps/browser/src/_locales/fr/messages.json | 27 +++++++ apps/browser/src/_locales/gl/messages.json | 27 +++++++ apps/browser/src/_locales/he/messages.json | 27 +++++++ apps/browser/src/_locales/hi/messages.json | 27 +++++++ apps/browser/src/_locales/hr/messages.json | 35 +++++++- apps/browser/src/_locales/hu/messages.json | 47 ++++++++--- apps/browser/src/_locales/id/messages.json | 27 +++++++ apps/browser/src/_locales/it/messages.json | 27 +++++++ apps/browser/src/_locales/ja/messages.json | 27 +++++++ apps/browser/src/_locales/ka/messages.json | 27 +++++++ apps/browser/src/_locales/km/messages.json | 27 +++++++ apps/browser/src/_locales/kn/messages.json | 27 +++++++ apps/browser/src/_locales/ko/messages.json | 27 +++++++ apps/browser/src/_locales/lt/messages.json | 27 +++++++ apps/browser/src/_locales/lv/messages.json | 27 +++++++ apps/browser/src/_locales/ml/messages.json | 27 +++++++ apps/browser/src/_locales/mr/messages.json | 27 +++++++ apps/browser/src/_locales/my/messages.json | 27 +++++++ apps/browser/src/_locales/nb/messages.json | 27 +++++++ apps/browser/src/_locales/ne/messages.json | 27 +++++++ apps/browser/src/_locales/nl/messages.json | 27 +++++++ apps/browser/src/_locales/nn/messages.json | 27 +++++++ apps/browser/src/_locales/or/messages.json | 27 +++++++ apps/browser/src/_locales/pl/messages.json | 27 +++++++ apps/browser/src/_locales/pt_BR/messages.json | 27 +++++++ apps/browser/src/_locales/pt_PT/messages.json | 31 +++++++- apps/browser/src/_locales/ro/messages.json | 27 +++++++ apps/browser/src/_locales/ru/messages.json | 27 +++++++ apps/browser/src/_locales/si/messages.json | 27 +++++++ apps/browser/src/_locales/sk/messages.json | 27 +++++++ apps/browser/src/_locales/sl/messages.json | 27 +++++++ apps/browser/src/_locales/sr/messages.json | 79 +++++++++++++------ apps/browser/src/_locales/sv/messages.json | 27 +++++++ apps/browser/src/_locales/ta/messages.json | 27 +++++++ apps/browser/src/_locales/te/messages.json | 27 +++++++ apps/browser/src/_locales/th/messages.json | 27 +++++++ apps/browser/src/_locales/tr/messages.json | 27 +++++++ apps/browser/src/_locales/uk/messages.json | 27 +++++++ apps/browser/src/_locales/vi/messages.json | 27 +++++++ apps/browser/src/_locales/zh_CN/messages.json | 35 +++++++- apps/browser/src/_locales/zh_TW/messages.json | 41 ++++++++-- apps/browser/store/locales/zh_TW/copy.resx | 58 +++++++------- 62 files changed, 1733 insertions(+), 86 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index b2d5563c545..397ea877cb5 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "تعديل" }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 66b7f948d92..c5a688f152f 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Axtarışı sıfırla" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Düzəliş et" }, diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 2da642ef6f2..44c82ef85b4 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Рэдагаваць" }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 3f77317f06e..a440690cee1 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Нулиране на търсенето" }, + "archive": { + "message": "Архивиране" + }, + "unarchive": { + "message": "Изваждане от архива" + }, + "itemsInArchive": { + "message": "Елементи в архива" + }, + "noItemsInArchive": { + "message": "Няма елементи в архива" + }, + "noItemsInArchiveDesc": { + "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." + }, + "itemSentToArchive": { + "message": "Елементът е преместен в архива" + }, + "itemRemovedFromArchive": { + "message": "Елементът е изваден от архива" + }, + "archiveItem": { + "message": "Архивиране на елемента" + }, + "archiveItemConfirmDesc": { + "message": "Архивираните елементи са изключени от общите резултати при търсене и от предложенията за автоматично попълване. Наистина ли искате да архивирате този елемент?" + }, "edit": { "message": "Редактиране" }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 40844b6e734..e7c4c36bce0 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "সম্পাদনা" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index e3dae509a9d..d9003a749a6 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index a79d0be6327..2002dfc467f 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Restableix la cerca" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edita" }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 5427289fdf4..0638257d687 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Resetovat hledání" }, + "archive": { + "message": "Archivovat" + }, + "unarchive": { + "message": "Odebrat z archivu" + }, + "itemsInArchive": { + "message": "Položky v archivu" + }, + "noItemsInArchive": { + "message": "Žádné položky v archivu" + }, + "noItemsInArchiveDesc": { + "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." + }, + "itemSentToArchive": { + "message": "Položka byla přesunuta do archivu" + }, + "itemRemovedFromArchive": { + "message": "Položka byla odebrána z archivu" + }, + "archiveItem": { + "message": "Archivovat položku" + }, + "archiveItemConfirmDesc": { + "message": "Archivované položky jsou vyloučeny z obecných výsledků vyhledávání a z návrhů automatického vyplňování. Jste si jisti, že chcete tuto položku archivovat?" + }, "edit": { "message": "Upravit" }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 6f670cec95f..8756a138e81 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Golygu" }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index e778685b566..a78ff26fb0f 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Redigér" }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 96dca6c3acb..f04ca5b11be 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Suche zurücksetzen" }, + "archive": { + "message": "Archivieren" + }, + "unarchive": { + "message": "Archivierung aufheben" + }, + "itemsInArchive": { + "message": "Einträge im Archiv" + }, + "noItemsInArchive": { + "message": "Kein Eintrag im Archiv" + }, + "noItemsInArchiveDesc": { + "message": "Archivierte Einträge erscheinen hier und werden von allgemeinen Suchergebnissen und Autofill Vorschlägen ausgeschlossen." + }, + "itemSentToArchive": { + "message": "Eintrag an das Archiv gesendet" + }, + "itemRemovedFromArchive": { + "message": "Eintrag aus dem Archiv entfernt" + }, + "archiveItem": { + "message": "Eintrag archivieren" + }, + "archiveItemConfirmDesc": { + "message": "Archivierte Einträge sind von allgemeinen Suchergebnissen und Autofill Vorschlägen ausgeschlossen. Sind Sie sicher, dass Sie diesen Eintrag archivieren möchten?" + }, "edit": { "message": "Bearbeiten" }, @@ -5512,16 +5539,16 @@ "message": "Willkommen in deinem Tresor!" }, "phishingPageTitle": { - "message": "Phishing website" + "message": "Phishing Webseite" }, "phishingPageCloseTab": { - "message": "Close tab" + "message": "Tab schließen" }, "phishingPageContinue": { - "message": "Continue" + "message": "Weiter" }, "phishingPageLearnWhy": { - "message": "Why are you seeing this?" + "message": "Warum sehen Sie das?" }, "hasItemsVaultNudgeBodyOne": { "message": "Einträge für die aktuelle Seite automatisch ausfüllen" diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index ce98d454084..cce3e0ea39f 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Επαναφορά αναζήτησης" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Επεξεργασία" }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index aab0cc92092..43bb17c297f 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 54ad3612f78..59c4966a48c 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 266a74b034a..d3c6e3556a0 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Restablecer búsqueda" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Editar" }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 7b9d096be82..5508a1cee72 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Muuda" }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 1ccd8f1849c..93242263dc0 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Editatu" }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 97b92fa8c29..129f2ee383a 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "ویرایش" }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 36c16254a67..5de1d9fe7e4 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Nollaa haku" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Muokkaa" }, diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 8a2c9912e5d..600abfb2d4e 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "I-edit" }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 3323995736b..765ebff53c5 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Réinitialiser la recherche" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Modifier" }, diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 947baabdc2c..c2573ea6bfa 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Editar" }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 6cdb3898962..38fe3618610 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "אפס חיפוש" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "ערוך" }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 215b5f88445..1575543aef3 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "खोज रीसेट करें" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "संपादन करें" }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index a37cc0cf368..4f67de34071 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Ponovno postavljanje pretraživanja" }, + "archive": { + "message": "Arhiviraj" + }, + "unarchive": { + "message": "Poništi arhiviranje" + }, + "itemsInArchive": { + "message": "Stavke u arhivi" + }, + "noItemsInArchive": { + "message": "Nema stavki u arhivi" + }, + "noItemsInArchiveDesc": { + "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." + }, + "itemSentToArchive": { + "message": "Stavka poslana u arhivu" + }, + "itemRemovedFromArchive": { + "message": "Stavka maknute iz arhive" + }, + "archiveItem": { + "message": "Arhiviraj stavku" + }, + "archiveItemConfirmDesc": { + "message": "Arhivirane stavke biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune. Sigurno želiš arhivirati?" + }, "edit": { "message": "Uredi" }, @@ -5512,16 +5539,16 @@ "message": "Dobrodošli u svoj trezor!" }, "phishingPageTitle": { - "message": "Phishing website" + "message": "Phishing web stranica" }, "phishingPageCloseTab": { - "message": "Close tab" + "message": "Zatvori karticu" }, "phishingPageContinue": { - "message": "Continue" + "message": "Nastavi" }, "phishingPageLearnWhy": { - "message": "Why are you seeing this?" + "message": "Zašto ovo vidiš?" }, "hasItemsVaultNudgeBodyOne": { "message": "Auto-ispuni stavke za trenutnu stranicu" diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 367a9fd1f9f..864580a64b0 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -6,11 +6,11 @@ "message": "Bitwarden logó" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden Jelszókezelő", "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": "Legyen otthon, munkában, vagy úton, a Bitwarden könnyen biztosítja jelszavát, kulcsait, és kényes információit", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -550,6 +550,33 @@ "resetSearch": { "message": "Keresés visszaállítása" }, + "archive": { + "message": "Archívum" + }, + "unarchive": { + "message": "Visszavétel archívumból" + }, + "itemsInArchive": { + "message": "Archívum elemek száma" + }, + "noItemsInArchive": { + "message": "Nincs elem az archívumban." + }, + "noItemsInArchiveDesc": { + "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." + }, + "itemSentToArchive": { + "message": "Archívumba küldött elemek száma" + }, + "itemRemovedFromArchive": { + "message": "Az elem kikerült a kedvencekből." + }, + "archiveItem": { + "message": "Elem archiválása" + }, + "archiveItemConfirmDesc": { + "message": "Az archivált elemek ki vannak zárva az általános keresési eredményekből és az automatikus kitöltési javaslatokból. Biztosan archiválni szeretnénk ezt az elemet?" + }, "edit": { "message": "Szerkesztés" }, @@ -866,13 +893,13 @@ "message": "Kijelentkezett" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Kijelentkezett fiókjából." }, "loginExpired": { "message": "Bejelentkezési munkamenete lejárt." }, "logIn": { - "message": "Log in" + "message": "Bejelentkezés" }, "logInToBitwarden": { "message": "Bejelentkezés a Bitwardenbe" @@ -896,16 +923,16 @@ "message": "Kövessük az alábbi lépéseket a biztonsági kulccsal bejelentkezés befejezéséhez." }, "restartRegistration": { - "message": "Restart registration" + "message": "Regisztráció újraindítása" }, "expiredLink": { - "message": "Expired link" + "message": "Lejárt hivatkozás" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "Kérem kezdje újra a regisztrációt, vagy próbáljon meg bejelentkezni." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "Lehetséges, hogy már rendelkezik fiókkal" }, "logOutConfirmation": { "message": "Biztos benne, hogy ki szeretnél jelentkezni?" @@ -1176,7 +1203,7 @@ "description": "Error message shown when the system fails to save login details." }, "saveFailureDetails": { - "message": "Oh no! We couldn't save this. Try entering the details manually.", + "message": "Ja ne! Nem tudtuk elmenteni. Próbálja meg beírni a kézzel.", "description": "Detailed error message shown when saving login details fails." }, "changePasswordWarning": { @@ -1606,7 +1633,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Automatikus kitöltés javaslatok" }, "autofillSpotlightTitle": { "message": "Az automatikus kitöltési javaslatok könnyű megtalálása" diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index c14294534df..b38b6f05628 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Atur ulang pencarian" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index ed699b61c91..df4411ee42b 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Svuota ricerca" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Modifica" }, diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 7137e7e1a90..5305a265781 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "編集" }, diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index c916c0d958e..b759d674cca 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "ჩასწორება" }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 0cdb46d102e..78a49021a0c 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 8e9d04688b1..1311a97df68 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "ಎಡಿಟ್" }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index f17371f28ba..06611be0282 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "편집" }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index f0f78f1de0f..464fa5aae92 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Keisti" }, diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 00708a95e41..99edb486d9d 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Atiestatīt meklēšanu" }, + "archive": { + "message": "Arhivēt" + }, + "unarchive": { + "message": "Atcelt arhivēšanu" + }, + "itemsInArchive": { + "message": "Vienumi arhīvā" + }, + "noItemsInArchive": { + "message": "Arhīvā nav vienumu" + }, + "noItemsInArchiveDesc": { + "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." + }, + "itemSentToArchive": { + "message": "Vienums ievietots arhīvā" + }, + "itemRemovedFromArchive": { + "message": "Vienums izņemts no arhīva" + }, + "archiveItem": { + "message": "Arhivēt vienumu" + }, + "archiveItemConfirmDesc": { + "message": "Arhivētie vienumi netiek iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos. Vai tiešām ahrivēt šo vienumu?" + }, "edit": { "message": "Labot" }, diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 8554d74db8c..efe18c96a59 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "തിരുത്തുക" }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 5dddf1f2bde..16ac31ff599 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 0cdb46d102e..78a49021a0c 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index c77bce78939..a23fd7fe4c1 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Rediger" }, diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 0cdb46d102e..78a49021a0c 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 379ed54e490..2562b7a1d4c 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Zoekopdracht resetten" }, + "archive": { + "message": "Archiveren" + }, + "unarchive": { + "message": "Dearchiveren" + }, + "itemsInArchive": { + "message": "Items in archief" + }, + "noItemsInArchive": { + "message": "Geen items in archief" + }, + "noItemsInArchiveDesc": { + "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." + }, + "itemSentToArchive": { + "message": "Item naar archief verzonden" + }, + "itemRemovedFromArchive": { + "message": "Item verwijderd uit archief" + }, + "archiveItem": { + "message": "Item archiveren" + }, + "archiveItemConfirmDesc": { + "message": "Gearchiveerde items worden uitgesloten van algemene zoekresultaten en automatische invulsuggesties. Weet je zeker dat je dit item wilt archiveren?" + }, "edit": { "message": "Bewerken" }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 0cdb46d102e..78a49021a0c 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 0cdb46d102e..78a49021a0c 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index b0f2b7017b7..f24e790c9ad 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Zresetuj wyszukiwanie" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Usuń z archiwum" + }, + "itemsInArchive": { + "message": "Elementy w archiwum" + }, + "noItemsInArchive": { + "message": "Brak elementów w archiwum" + }, + "noItemsInArchiveDesc": { + "message": "Zarchiwizowane elementy pojawią się tutaj i zostaną wykluczone z wyników wyszukiwania i sugestii autouzupełniania." + }, + "itemSentToArchive": { + "message": "Element został przeniesiony do archiwum" + }, + "itemRemovedFromArchive": { + "message": "Element został usunięty z archiwum" + }, + "archiveItem": { + "message": "Archiwizuj element" + }, + "archiveItemConfirmDesc": { + "message": "Zarchiwizowane elementy są wykluczone z wyników wyszukiwania i sugestii autouzupełniania. Czy na pewno chcesz archiwizować element?" + }, "edit": { "message": "Edytuj" }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 5fd9d1673b6..2d7dd1e42a4 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Editar" }, diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 1a8ea3bfb3c..acc5b5332f9 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Repor pesquisa" }, + "archive": { + "message": "Arquivar" + }, + "unarchive": { + "message": "Desarquivar" + }, + "itemsInArchive": { + "message": "Itens no arquivo" + }, + "noItemsInArchive": { + "message": "Nenhum item no arquivo" + }, + "noItemsInArchiveDesc": { + "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." + }, + "itemSentToArchive": { + "message": "Item movido para o arquivo" + }, + "itemRemovedFromArchive": { + "message": "Item removido do arquivo" + }, + "archiveItem": { + "message": "Arquivar item" + }, + "archiveItemConfirmDesc": { + "message": "Os itens arquivados são excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático. Tem a certeza de que pretende arquivar este item?" + }, "edit": { "message": "Editar" }, @@ -1235,10 +1262,10 @@ "message": "Tema" }, "themeDesc": { - "message": "Altere o tema de cores da aplicação." + "message": "Altere o tema da aplicação." }, "themeDescAlt": { - "message": "Altere o tema de cores da aplicação. Aplica-se a todas as contas com sessão iniciada." + "message": "Altere o tema da aplicação. Aplica-se a todas as contas com sessão iniciada." }, "dark": { "message": "Escuro", diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index e57fe4dd19c..d184460e293 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Editare" }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 485665524d5..17133350e3f 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Сбросить поиск" }, + "archive": { + "message": "Архив" + }, + "unarchive": { + "message": "Разархивировать" + }, + "itemsInArchive": { + "message": "Элементы в архиве" + }, + "noItemsInArchive": { + "message": "В архиве нет элементов" + }, + "noItemsInArchiveDesc": { + "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." + }, + "itemSentToArchive": { + "message": "Элемент отправлен в архив" + }, + "itemRemovedFromArchive": { + "message": "Элемент удален из архива" + }, + "archiveItem": { + "message": "Архивировать элемент" + }, + "archiveItemConfirmDesc": { + "message": "Архивированные элементы исключены из общих результатов поиска и предложений автозаполнения. Вы уверены, что хотите архивировать этот элемент?" + }, "edit": { "message": "Изменить" }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 9e6f27bab83..2fd8f53e148 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "සංස්කරණය" }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 7d410f67db3..d0e143cce4a 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Resetovať vyhľadávanie" }, + "archive": { + "message": "Archivovať" + }, + "unarchive": { + "message": "Zrušiť archiváciu" + }, + "itemsInArchive": { + "message": "Položky v archíve" + }, + "noItemsInArchive": { + "message": "Žiadne položky v archíve" + }, + "noItemsInArchiveDesc": { + "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." + }, + "itemSentToArchive": { + "message": "Položka bola archivovaná" + }, + "itemRemovedFromArchive": { + "message": "Položka bola odobraná z archívu" + }, + "archiveItem": { + "message": "Archivovať položku" + }, + "archiveItemConfirmDesc": { + "message": "Archivované položky sú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania. Naozaj chcete archivovať túto položku?" + }, "edit": { "message": "Upraviť" }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index d742e0a4b2d..81b1a6bb52c 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Uredi" }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 5de4d4da336..cc4abafe878 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Ресетовати претрагу" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Уреди" }, @@ -1755,7 +1782,7 @@ "message": "Ако кликнете изван искачућег прозора да бисте проверили имејл за верификациони код, овај прозор ће се затворити. Да ли желите да отворите овај прозор у новом прозору да се не би затворио?" }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Прикажи иконе веб локација и преузмите линкове промене лозинке" }, "cardholderName": { "message": "Име Власника Картице" @@ -1920,79 +1947,79 @@ "message": "Белешка" }, "newItemHeaderLogin": { - "message": "New Login", + "message": "Ново пријављивање", "description": "Header for new login item type" }, "newItemHeaderCard": { - "message": "New Card", + "message": "Нова картица", "description": "Header for new card item type" }, "newItemHeaderIdentity": { - "message": "New Identity", + "message": "Нови идентитет", "description": "Header for new identity item type" }, "newItemHeaderNote": { - "message": "New Note", + "message": "Нова белешка", "description": "Header for new note item type" }, "newItemHeaderSshKey": { - "message": "New SSH key", + "message": "Нов SSH кљич", "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "New Text Send", + "message": "Нови текст Send", "description": "Header for new text send" }, "newItemHeaderFileSend": { - "message": "New File Send", + "message": "Нова датотека Send", "description": "Header for new file send" }, "editItemHeaderLogin": { - "message": "Edit Login", + "message": "Уреди пријаву", "description": "Header for edit login item type" }, "editItemHeaderCard": { - "message": "Edit Card", + "message": "Уреди картицу", "description": "Header for edit card item type" }, "editItemHeaderIdentity": { - "message": "Edit Identity", + "message": "Уреди идентитет", "description": "Header for edit identity item type" }, "editItemHeaderNote": { - "message": "Edit Note", + "message": "Уреди белешку", "description": "Header for edit note item type" }, "editItemHeaderSshKey": { - "message": "Edit SSH key", + "message": "Уреди SSH кључ", "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Edit Text Send", + "message": "Уреди текст Send", "description": "Header for edit text send" }, "editItemHeaderFileSend": { - "message": "Edit File Send", + "message": "Уреди датотеку Send", "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "Преглед пријаве", "description": "Header for view login item type" }, "viewItemHeaderCard": { - "message": "View Card", + "message": "Преглед картице", "description": "Header for view card item type" }, "viewItemHeaderIdentity": { - "message": "View Identity", + "message": "Преглед идентитета", "description": "Header for view identity item type" }, "viewItemHeaderNote": { - "message": "View Note", + "message": "Преглед белешке", "description": "Header for view note item type" }, "viewItemHeaderSshKey": { - "message": "View SSH key", + "message": "Преглед SSH кључа", "description": "Header for view SSH key item type" }, "passwordHistory": { @@ -5512,16 +5539,16 @@ "message": "Добродошли у ваш сеф!" }, "phishingPageTitle": { - "message": "Phishing website" + "message": "Пронађен злонамеран сајт" }, "phishingPageCloseTab": { - "message": "Close tab" + "message": "Затвори језичак" }, "phishingPageContinue": { - "message": "Continue" + "message": "Настави" }, "phishingPageLearnWhy": { - "message": "Why are you seeing this?" + "message": "Зашто видите ово?" }, "hasItemsVaultNudgeBodyOne": { "message": "Ауто-пуњење предмета за тренутну страницу" @@ -5599,10 +5626,10 @@ "description": "Aria label for the body content of the generator nudge" }, "aboutThisSetting": { - "message": "About this setting" + "message": "О овом подешавању" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden ће користити сачуване URI-јеве за пријаву да би одредио коју икону или URL за промену лозинке треба користити како би побољшао ваше искуство. Никакви подаци нису сакупљени нити сачувани приликом коришћења ове услуге." }, "noPermissionsViewPage": { "message": "Немате дозволе за преглед ове странице. Покушајте да се пријавите са другим налогом." diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 91c8919c810..8b0263bf15a 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Nollställ sökning" }, + "archive": { + "message": "Arkivera" + }, + "unarchive": { + "message": "Packa upp" + }, + "itemsInArchive": { + "message": "Objekt i arkiv" + }, + "noItemsInArchive": { + "message": "Inga objekt i arkivet" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Arkivera objekt" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Redigera" }, diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index 5660467e3ff..8d2199db6ca 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "தேடலை மீட்டமை" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "திருத்து" }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 0cdb46d102e..78a49021a0c 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 93f064c6801..61f97564f6a 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Reset search" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "แก้ไข" }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index ce3d485a937..0b65ae7d476 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Aramayı sıfırla" }, + "archive": { + "message": "Arşivle" + }, + "unarchive": { + "message": "Arşivden çıkar" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Düzenle" }, diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 24f7f71b9b2..850c174f666 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Скинути пошук" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Змінити" }, diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 57d404438ef..76bea4120cd 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "Đặt lại tìm kiếm" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "Sửa" }, diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 9e8ce80a29b..051914b120c 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "重置搜索" }, + "archive": { + "message": "归档" + }, + "unarchive": { + "message": "取消归档" + }, + "itemsInArchive": { + "message": "归档中的项目" + }, + "noItemsInArchive": { + "message": "归档中没有项目" + }, + "noItemsInArchiveDesc": { + "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" + }, + "itemSentToArchive": { + "message": "项目已归档" + }, + "itemRemovedFromArchive": { + "message": "项目已取消归档" + }, + "archiveItem": { + "message": "归档项目" + }, + "archiveItemConfirmDesc": { + "message": "已归档的项目将被排除在一般搜索结果和自动填充建议之外。确定要归档此项目吗?" + }, "edit": { "message": "编辑" }, @@ -814,7 +841,7 @@ "message": "您可以关闭此窗口" }, "masterPassSent": { - "message": "我们已经为您发送了包含主密码提示的电子邮件。" + "message": "我们已经向您发送了一封包含主密码提示的电子邮件。" }, "verificationCodeRequired": { "message": "必须填写验证码。" @@ -1755,7 +1782,7 @@ "message": "如果您点击弹窗外的区域以检查您的验证码电子邮件,将导致弹窗关闭。您想在新窗口中打开此弹窗,以便它不会关闭吗?" }, "showIconsChangePasswordUrls": { - "message": "显示网站图标并检索更改密码的 URL" + "message": "显示网站图标并获取更改密码的 URL" }, "cardholderName": { "message": "持卡人姓名" @@ -4376,7 +4403,7 @@ "message": "仅此一次" }, "alwaysForThisSite": { - "message": "总是为此站点" + "message": "始终适用于此站点" }, "domainAddedToExcludedDomains": { "message": "$DOMAIN$ 已添加到排除域名列表。", @@ -5521,7 +5548,7 @@ "message": "继续" }, "phishingPageLearnWhy": { - "message": "您为什么会看到这个?" + "message": "为什么您会看到这个?" }, "hasItemsVaultNudgeBodyOne": { "message": "为当前页面自动填充项目" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index b528695ccc4..f5801fb2c7d 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -550,6 +550,33 @@ "resetSearch": { "message": "重設搜尋" }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, "edit": { "message": "編輯" }, @@ -1049,10 +1076,10 @@ "message": "於分頁頁面顯示身分以便於自動填入。" }, "clickToAutofillOnVault": { - "message": "在密碼庫檢視中點擊項目來自動填入" + "message": "在密碼庫檢視中點選項目來自動填入" }, "clickToAutofill": { - "message": "Click items in autofill suggestion to fill" + "message": "點選自動填入建議中的項目進行填入" }, "clearClipboard": { "message": "清除剪貼簿", @@ -1562,16 +1589,16 @@ "message": "輸入寄送到您電子郵件信箱的驗證碼。" }, "selfHostedEnvironment": { - "message": "自我裝載環境" + "message": "自行部署環境" }, "selfHostedBaseUrlHint": { - "message": "指定您自建的 Bitwarden 伺服器的網域 URL。例如:https://bitwarden.company.com" + "message": "指定您自架的 Bitwarden 伺服器的網域 URL。例如:https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { "message": "適用於進階設定。您可以單獨指定各個服務的網域 URL。" }, "selfHostedEnvFormInvalid": { - "message": "您必須新增伺服器網域 URL 或至少一個自定義環境。" + "message": "您必須新增伺服器網域 URL 或至少一個自訂環境。" }, "customEnvironment": { "message": "自訂環境" @@ -1580,7 +1607,7 @@ "message": "伺服器 URL" }, "selfHostBaseUrl": { - "message": "自建伺服器 URL", + "message": "自架伺服器 URL", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -5134,7 +5161,7 @@ } }, "showQuickCopyActions": { - "message": "在密碼庫中顯示快速複製" + "message": "在密碼庫中顯示快速複製圖示" }, "systemDefault": { "message": "系統預設值" diff --git a/apps/browser/store/locales/zh_TW/copy.resx b/apps/browser/store/locales/zh_TW/copy.resx index ad3f12ae6f0..bfabc13f280 100644 --- a/apps/browser/store/locales/zh_TW/copy.resx +++ b/apps/browser/store/locales/zh_TW/copy.resx @@ -121,55 +121,55 @@ Bitwarden 密碼管理工具 - 無論在家、在辦公或在途中,Bitwarden 都能輕易的保護你的密碼、登入金鑰和敏感資訊。 + 無論在家中、工作中抑或旅途中,Bitwarden 都能輕鬆地保護您的密碼、登入金鑰和敏感資訊。 - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + 被 PCMag、WIRED、The Verge、CNET、G2 等認可為最佳的密碼管理工具! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +保護您的數位生活 +透過為每個帳戶產生並保存唯一的強密碼,保護您的數位生活並防止資料外洩。將所有內容儲存在只有您可以存取的端對端加密密碼庫中。 -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +隨時隨地在任何裝置上存取您的資料 +不受限制地跨裝置輕鬆管理、儲存、保護和分享無限多的密碼。 -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +每個人都應該擁有保持上網安全的工具 +免費使用 Bitwarden,沒有廣告或銷售資料。Bitwarden 認為每個人都應該有能力確保上網安全。進階版計劃提供對進階功能的存取。 -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +透過 BITWARDEN 強化您的團隊 +團隊和企業計劃具有專業的商業功能。包括 SSO 整合、自架服務、目錄整合和 SCIM 配置、全域原則、API 存取、事件記錄等。 -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +使用 Bitwarden 來保護您的員工並與同事分享敏感資訊。 -More reasons to choose Bitwarden: +選擇 Bitwarden 的其他理由: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +世界級的加密 +密碼受到進階端對端加密(AES-256 位元加密、加鹽雜湊和 PBKDF2 SHA-256)的保護,因此您的資料保持安全和私密。 -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +第三方稽核 +Bitwarden 定期與知名資安公司進行全面的第三方安全稽核。這些年度稽核包括 Bitwarden IP、伺服器和 Web 應用程式的原始程式碼評估和滲透測試。 -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +進階的雙重要素驗證 +使用第三方驗證器、透過電子郵件傳送的代碼或 FIDO2 WebAuthn 憑證(例如硬體安全金鑰或密碼)來保護您的登入。 -Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Bitwarden 傳送 +直接將資料傳輸給其他人,同時保持端對端加密安全性並限制暴露。 -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +內建產生器 +為您造訪的每個網站建立長、複雜且獨特的密碼和唯一的使用者名稱。與電子郵件別名提供者整合以進一步保護隱私。 -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +全球化翻譯 +Bitwarden 有 60 多種語言翻譯,由全球社群透過 Crowdin 翻譯。 -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +跨平台應用程式 +透過任何瀏覽器、行動裝置或桌面作業系統等,保護和共用 Bitwarden 密碼庫中的敏感資料。 -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden 保護的不僅是密碼 +Bitwarden 的端對端加密憑證管理解決方案可讓組織保護一切,包括開發人員機密和金鑰體驗。造訪 Bitwarden.com 以了解更多有關 Bitwarden 機密管理員和 Bitwarden Passwordless.dev 的資訊! - 無論在家、在辦公或在途中,Bitwarden 都能輕易的保護你的密碼、登入金鑰和敏感資訊。 + 無論在家中、工作中抑或旅途中,Bitwarden 都能輕鬆地保護您的密碼、登入金鑰和敏感資訊。 在多部裝置上同步和存取密碼庫 From 82b75b72afb1fd099dfb742e200bd927d7176102 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:07:56 +0200 Subject: [PATCH 10/55] Autosync the updated translations (#16606) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 27 ++++++++++ apps/desktop/src/locales/ar/messages.json | 27 ++++++++++ apps/desktop/src/locales/az/messages.json | 27 ++++++++++ apps/desktop/src/locales/be/messages.json | 27 ++++++++++ apps/desktop/src/locales/bg/messages.json | 27 ++++++++++ apps/desktop/src/locales/bn/messages.json | 27 ++++++++++ apps/desktop/src/locales/bs/messages.json | 27 ++++++++++ apps/desktop/src/locales/ca/messages.json | 53 +++++++++++++++----- apps/desktop/src/locales/cs/messages.json | 27 ++++++++++ apps/desktop/src/locales/cy/messages.json | 27 ++++++++++ apps/desktop/src/locales/da/messages.json | 27 ++++++++++ apps/desktop/src/locales/de/messages.json | 27 ++++++++++ apps/desktop/src/locales/el/messages.json | 27 ++++++++++ apps/desktop/src/locales/en_GB/messages.json | 27 ++++++++++ apps/desktop/src/locales/en_IN/messages.json | 27 ++++++++++ apps/desktop/src/locales/eo/messages.json | 27 ++++++++++ apps/desktop/src/locales/es/messages.json | 27 ++++++++++ apps/desktop/src/locales/et/messages.json | 27 ++++++++++ apps/desktop/src/locales/eu/messages.json | 27 ++++++++++ apps/desktop/src/locales/fa/messages.json | 27 ++++++++++ apps/desktop/src/locales/fi/messages.json | 27 ++++++++++ apps/desktop/src/locales/fil/messages.json | 27 ++++++++++ apps/desktop/src/locales/fr/messages.json | 27 ++++++++++ apps/desktop/src/locales/gl/messages.json | 27 ++++++++++ apps/desktop/src/locales/he/messages.json | 27 ++++++++++ apps/desktop/src/locales/hi/messages.json | 27 ++++++++++ apps/desktop/src/locales/hr/messages.json | 27 ++++++++++ apps/desktop/src/locales/hu/messages.json | 27 ++++++++++ apps/desktop/src/locales/id/messages.json | 27 ++++++++++ apps/desktop/src/locales/it/messages.json | 27 ++++++++++ apps/desktop/src/locales/ja/messages.json | 27 ++++++++++ apps/desktop/src/locales/ka/messages.json | 27 ++++++++++ apps/desktop/src/locales/km/messages.json | 27 ++++++++++ apps/desktop/src/locales/kn/messages.json | 27 ++++++++++ apps/desktop/src/locales/ko/messages.json | 27 ++++++++++ apps/desktop/src/locales/lt/messages.json | 27 ++++++++++ apps/desktop/src/locales/lv/messages.json | 27 ++++++++++ apps/desktop/src/locales/me/messages.json | 27 ++++++++++ apps/desktop/src/locales/ml/messages.json | 27 ++++++++++ apps/desktop/src/locales/mr/messages.json | 27 ++++++++++ apps/desktop/src/locales/my/messages.json | 27 ++++++++++ apps/desktop/src/locales/nb/messages.json | 27 ++++++++++ apps/desktop/src/locales/ne/messages.json | 27 ++++++++++ apps/desktop/src/locales/nl/messages.json | 27 ++++++++++ apps/desktop/src/locales/nn/messages.json | 27 ++++++++++ apps/desktop/src/locales/or/messages.json | 27 ++++++++++ apps/desktop/src/locales/pl/messages.json | 41 ++++++++++++--- apps/desktop/src/locales/pt_BR/messages.json | 27 ++++++++++ apps/desktop/src/locales/pt_PT/messages.json | 31 +++++++++++- apps/desktop/src/locales/ro/messages.json | 27 ++++++++++ apps/desktop/src/locales/ru/messages.json | 31 +++++++++++- apps/desktop/src/locales/si/messages.json | 27 ++++++++++ apps/desktop/src/locales/sk/messages.json | 27 ++++++++++ apps/desktop/src/locales/sl/messages.json | 27 ++++++++++ apps/desktop/src/locales/sr/messages.json | 43 +++++++++++++--- apps/desktop/src/locales/sv/messages.json | 27 ++++++++++ apps/desktop/src/locales/ta/messages.json | 27 ++++++++++ apps/desktop/src/locales/te/messages.json | 27 ++++++++++ apps/desktop/src/locales/th/messages.json | 27 ++++++++++ apps/desktop/src/locales/tr/messages.json | 27 ++++++++++ apps/desktop/src/locales/uk/messages.json | 27 ++++++++++ apps/desktop/src/locales/vi/messages.json | 27 ++++++++++ apps/desktop/src/locales/zh_CN/messages.json | 31 +++++++++++- apps/desktop/src/locales/zh_TW/messages.json | 35 +++++++++++-- 64 files changed, 1766 insertions(+), 38 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index b6869f4f048..e579c498ded 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 37f338ff65e..4efec524886 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 889cea55a2b..4b6a3cfa416 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Qısayola düzəliş et" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index b1c44ddd730..eb5971c97af 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 71faa0fbe74..e32363f0c55 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Редактиране на комбинацията" + }, + "archive": { + "message": "Архивиране" + }, + "unarchive": { + "message": "Изваждане от архива" + }, + "itemsInArchive": { + "message": "Елементи в архива" + }, + "noItemsInArchive": { + "message": "Няма елементи в архива" + }, + "noItemsInArchiveDesc": { + "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." + }, + "itemSentToArchive": { + "message": "Елементът е преместен в архива" + }, + "itemRemovedFromArchive": { + "message": "Елементът е изваден от архива" + }, + "archiveItem": { + "message": "Архивиране на елемента" + }, + "archiveItemConfirmDesc": { + "message": "Архивираните елементи са изключени от общите резултати при търсене и от предложенията за автоматично попълване. Наистина ли искате да архивирате този елемент?" } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 583377ac4ca..60b925af2e3 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 56ac07e0f92..e6cdff50696 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index bafe54322b6..0defa7a878a 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1303,7 +1303,7 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Mostra icones de llocs web i recupera les URL de canvi de contrasenya" }, "enableMinToTray": { "message": "Minimitza a icona en la safata" @@ -2416,16 +2416,16 @@ "message": "La vostra contrasenya mestra no compleix una o més de les polítiques de l'organització. Per accedir a la caixa forta, heu d'actualitzar-la ara. Si continueu, es tancarà la sessió actual i us demanarà que torneu a iniciar-la. Les sessions en altres dispositius poden continuar romanent actives fins a una hora." }, "changePasswordWarning": { - "message": "En canviar la teva contrasenya, cal iniciar la sessió amb la nova contrasenya. Les sessions actives en altres dispositius es tancaran en una hora." + "message": "En canviar la contrasenya, cal iniciar la sessió amb la nova contrasenya. Les sessions actives en altres dispositius es tancaran en una hora." }, "accountRecoveryUpdateMasterPasswordSubtitle": { - "message": "Canvia la contrasenya mestra per completar el recobrament del compte." + "message": "Canvia la contrasenya mestra per completar la recuperació del compte." }, "updateMasterPasswordSubtitle": { - "message": "La contrasenya mestra no s'ajusta als requisits de l'organització. Canvia't la contrasenya mestra per continuar." + "message": "La contrasenya mestra no s'ajusta als requisits de l'organització. Canvieu-la per continuar." }, "tdeDisabledMasterPasswordRequired": { - "message": "La teva organització ha desactivat l'encriptació de dispositius fiables. Fixa una contrasenya mestra per accedir a la teva caixa forta." + "message": "L'organització ha desactivat el xifratge de dispositius de confiança. Defineix una contrasenya mestra per accedir a la caixa forta." }, "tryAgain": { "message": "Torneu-ho a provar" @@ -2470,7 +2470,7 @@ "message": "Minuts" }, "vaultTimeoutPolicyInEffect1": { - "message": "$HOURS$ hora(es) i $MINUTES$ minut(s) màxim.", + "message": "$HOURS$ hores i $MINUTES$ minuts com a màxim.", "placeholders": { "hours": { "content": "$1", @@ -2565,7 +2565,7 @@ "message": "S'ha suprimit la contrasenya mestra." }, "removeMasterPasswordForOrganizationUserKeyConnector": { - "message": "Ja no cal contrasenya mestra per als membres de la següent organització. Confirma'n el domini a sota amb l'administrador de la teva organització." + "message": "Ja no cal contrasenya mestra per als membres de la següent organització. Confirmeu el domini següent amb l'administrador de l'organització." }, "organizationName": { "message": "Nom de l'organització" @@ -2634,7 +2634,7 @@ } }, "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Només els objectes individuals de la caixa forta, inclosos adjunts associats amb $EMAIL$, seran exportats. Els objectes de la caixa forta de l'organització no hi seran inclosos", + "message": "Només s'exportaran els elements individuals de la caixa forta, inclosos els fitxers adjunts associats amb $EMAIL$. No s'hi inclouran els elements de la caixa forta de l'organització", "placeholders": { "email": { "content": "$1", @@ -2760,7 +2760,7 @@ "message": "Utilitzeu aquesta contrasenya" }, "useThisPassphrase": { - "message": "Empra aquesta frase de pas" + "message": "Utilitzeu aquesta frase de contrasenya" }, "useThisUsername": { "message": "Utilitzeu aquest nom d'usuari" @@ -2829,7 +2829,7 @@ } }, "forwaderInvalidToken": { - "message": "API token de $SERVICENAME$ invàlid", + "message": "Token d'API $SERVICENAME$ no vàlid", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -3453,10 +3453,10 @@ "message": "Cal l'inici de sessió en dos passos de Duo al vostre compte. Seguiu els passos de sota per finalitzar l'inici de sessió." }, "followTheStepsBelowToFinishLoggingIn": { - "message": "Seguiu els passos de sota per finalitzar l'inici de sessió." + "message": "Seguiu els passos següents per finalitzar l'inici de sessió." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Seguiu els passos de sota per finalitzar l'inici de sessió amb la clau de seguretat." + "message": "Seguiu els passos següents per finalitzar l'inici de sessió amb la clau de seguretat." }, "launchDuo": { "message": "Inicia Duo al navegador" @@ -3627,7 +3627,7 @@ "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { - "message": "Més sobre la detecció de coincidències", + "message": "Més informació sobre la detecció de coincidències", "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 06b1ffa2aed..2c5ed437187 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Upravit zkratku" + }, + "archive": { + "message": "Archivovat" + }, + "unarchive": { + "message": "Odebrat z archivu" + }, + "itemsInArchive": { + "message": "Položky v archivu" + }, + "noItemsInArchive": { + "message": "Žádné položky v archivu" + }, + "noItemsInArchiveDesc": { + "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." + }, + "itemSentToArchive": { + "message": "Položka byla přesunuta do archivu" + }, + "itemRemovedFromArchive": { + "message": "Položka byla odebrána z archivu" + }, + "archiveItem": { + "message": "Archivovat položku" + }, + "archiveItemConfirmDesc": { + "message": "Archivované položky jsou vyloučeny z obecných výsledků vyhledávání a z návrhů automatického vyplňování. Jste si jisti, že chcete tuto položku archivovat?" } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 0e0ae3d6411..9ff42bfa2c7 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index d1e3e3ece89..4a064a004cb 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index b2d8dada299..47b3bad34e8 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 55298aed348..0c2ee1fab65 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 267116025d1..625d8804676 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 44b5000bfde..41211c2e7d7 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index d3a2bb1b649..cebc2fa1432 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 096639cd08f..346dc0d4221 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 107c5245d1c..b8afeb2ed6a 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index f4138389253..7f719ec0a4b 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 7239c9b75d5..fbbbdfd8c7f 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index aadf705cd13..ecde260d80e 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 6d75cd274ad..5ad2661b46a 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 119f6638079..dee70397307 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Modifier le raccourci" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 27394d39ece..5849d9d4cee 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index ea6528e3f86..5cbceb3ad76 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "ערוך קיצור דרך" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 7cbf25e24d4..25ecbdf3840 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index b88444f948f..64f89a8b15f 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Uredi prečac" + }, + "archive": { + "message": "Arhiviraj" + }, + "unarchive": { + "message": "Poništi arhiviranje" + }, + "itemsInArchive": { + "message": "Stavke u arhivi" + }, + "noItemsInArchive": { + "message": "Nema stavki u arhivi" + }, + "noItemsInArchiveDesc": { + "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." + }, + "itemSentToArchive": { + "message": "Stavka poslana u arhivu" + }, + "itemRemovedFromArchive": { + "message": "Stavka maknute iz arhive" + }, + "archiveItem": { + "message": "Arhiviraj stavku" + }, + "archiveItemConfirmDesc": { + "message": "Arhivirane stavke biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune. Sigurno želiš arhivirati?" } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 1bedc36c8d2..9f71448ce5a 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Parancsikon szerkesztése" + }, + "archive": { + "message": "Archívum" + }, + "unarchive": { + "message": "Visszavétel archívumból" + }, + "itemsInArchive": { + "message": "Archívum elemek száma" + }, + "noItemsInArchive": { + "message": "Nincs elem az archívumban." + }, + "noItemsInArchiveDesc": { + "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." + }, + "itemSentToArchive": { + "message": "Archívumba küldött elemek száma" + }, + "itemRemovedFromArchive": { + "message": "Az elem kikerült az archívumból." + }, + "archiveItem": { + "message": "Elem archiválása" + }, + "archiveItemConfirmDesc": { + "message": "Az archivált elemek ki vannak zárva az általános keresési eredményekből és az automatikus kitöltési javaslatokból. Biztosan archiválni szeretnénk ezt az elemet?" } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 23292b356bc..3f44fd8bf97 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 72db169d7ca..780d09f3582 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index ed030ae71d5..a58543302fa 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index c33e414ac3f..0bb7e929979 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 27394d39ece..5849d9d4cee 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index f09d2e67167..66a6e43d0cb 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index fdd0b055665..59423b8ad73 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index b377ab195b0..8159bc5e28b 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 97f239b56cc..5e29f10190b 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Labot saīsni" + }, + "archive": { + "message": "Arhivēt" + }, + "unarchive": { + "message": "Atcelt arhivēšanu" + }, + "itemsInArchive": { + "message": "Vienumi arhīvā" + }, + "noItemsInArchive": { + "message": "Arhīvā nav vienumu" + }, + "noItemsInArchiveDesc": { + "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." + }, + "itemSentToArchive": { + "message": "Vienums ievietots arhīvā" + }, + "itemRemovedFromArchive": { + "message": "Vienums izņemts no arhīva" + }, + "archiveItem": { + "message": "Arhivēt vienumu" + }, + "archiveItemConfirmDesc": { + "message": "Arhivētie vienumi netiek iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos. Vai tiešām ahrivēt šo vienumu?" } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 9e87e473af4..b023d0efab0 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 2e191908bc9..863f3941a0f 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 27394d39ece..5849d9d4cee 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 4e9bab677a6..fae67d310f1 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 03783474190..ec1c1bdb9b5 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 916becbf94d..813fa967252 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index ff4a7a6b7bb..c726c003776 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Snelkoppeling bewerken" + }, + "archive": { + "message": "Archiveren" + }, + "unarchive": { + "message": "Dearchiveren" + }, + "itemsInArchive": { + "message": "Items in archief" + }, + "noItemsInArchive": { + "message": "Geen items in archief" + }, + "noItemsInArchiveDesc": { + "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." + }, + "itemSentToArchive": { + "message": "Item naar archief verzonden" + }, + "itemRemovedFromArchive": { + "message": "Item verwijderd uit archief" + }, + "archiveItem": { + "message": "Item archiveren" + }, + "archiveItemConfirmDesc": { + "message": "Gearchiveerde items worden uitgesloten van algemene zoekresultaten en automatische invulsuggesties. Weet je zeker dat je dit item wilt archiveren?" } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index f625cb54c9f..94c2196edfa 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 4a6debac4bf..217439bec80 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 37f6263064a..6caee67447f 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -1303,7 +1303,7 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Pokaż ikony stron internetowych i pobierz adresy URL do zmiany hasła" }, "enableMinToTray": { "message": "Minimalizuj do zasobnika systemowego" @@ -3586,10 +3586,10 @@ "message": "Kontynuuj logowanie przy użyciu danych firmowych." }, "importDirectlyFromBrowser": { - "message": "Import directly from browser" + "message": "Importuj bezpośrednio z przeglądarki" }, "browserProfile": { - "message": "Browser Profile" + "message": "Profil przeglądarki" }, "seeDetailedInstructions": { "message": "Zobacz szczegółowe instrukcje na naszej stronie pomocy pod adresem", @@ -3834,10 +3834,10 @@ "message": "Zmień zagrożone hasło" }, "changeAtRiskPasswordAndAddWebsite": { - "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." + "message": "Dane logowania są zagrożone i nie zawierają strony internetowej. Dodaj stronę internetową i zmień hasło." }, "missingWebsite": { - "message": "Missing website" + "message": "Brak strony internetowej" }, "cannotRemoveViewOnlyCollections": { "message": "Nie możesz usunąć następujących kolekcji z uprawnieniami tylko do odczytu: $COLLECTIONS$", @@ -3935,10 +3935,10 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "aboutThisSetting": { - "message": "About this setting" + "message": "O ustawieniu" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden użyje zapisanych adresów URL danych logowania, aby określić, która ikona lub adres URL zmiany hasła powinien zostać użyty w celu poprawy komfortu użytkowania. Usługa nie zapisuje żadnych danych." }, "assignToCollections": { "message": "Przypisz do kolekcji" @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edytuj skrót" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Usuń z archiwum" + }, + "itemsInArchive": { + "message": "Elementy w archiwum" + }, + "noItemsInArchive": { + "message": "Brak elementów w archiwum" + }, + "noItemsInArchiveDesc": { + "message": "Zarchiwizowane elementy pojawią się tutaj i zostaną wykluczone z wyników wyszukiwania i sugestii autouzupełniania." + }, + "itemSentToArchive": { + "message": "Element został przeniesiony do archiwum" + }, + "itemRemovedFromArchive": { + "message": "Element został usunięty z archiwum" + }, + "archiveItem": { + "message": "Archiwizuj element" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index b00cd3a8087..4b28ca4918a 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 8cef762328e..49573dcd647 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1369,13 +1369,13 @@ "message": "Idioma" }, "languageDesc": { - "message": "Alterar o idioma utilizado pela aplicação. É necessário reiniciar." + "message": "Altere o idioma utilizado pela aplicação. É necessário reiniciar." }, "theme": { "message": "Tema" }, "themeDesc": { - "message": "Altere o tema de cores da aplicação." + "message": "Altere o tema da aplicação." }, "dark": { "message": "Escuro", @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Editar atalho" + }, + "archive": { + "message": "Arquivar" + }, + "unarchive": { + "message": "Desarquivar" + }, + "itemsInArchive": { + "message": "Itens no arquivo" + }, + "noItemsInArchive": { + "message": "Nenhum item no arquivo" + }, + "noItemsInArchiveDesc": { + "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." + }, + "itemSentToArchive": { + "message": "Item movido para o arquivo" + }, + "itemRemovedFromArchive": { + "message": "Item removido do arquivo" + }, + "archiveItem": { + "message": "Arquivar item" + }, + "archiveItemConfirmDesc": { + "message": "Os itens arquivados são excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático. Tem a certeza de que pretende arquivar este item?" } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 0d2c02854bf..802afc3ef22 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index e2dfbdd3fcc..10c29017c46 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -3834,10 +3834,10 @@ "message": "Изменить пароль, подверженный риску" }, "changeAtRiskPasswordAndAddWebsite": { - "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." + "message": "Этот логин находится под угрозой и у него отсутствует веб-сайт. Добавьте веб-сайт и смените пароль для большей безопасности." }, "missingWebsite": { - "message": "Missing website" + "message": "Отсутствует сайт" }, "cannotRemoveViewOnlyCollections": { "message": "Вы не можете удалить коллекции с правами только на просмотр: $COLLECTIONS$", @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Изменить ярлык" + }, + "archive": { + "message": "Архив" + }, + "unarchive": { + "message": "Разархивировать" + }, + "itemsInArchive": { + "message": "Элементы в архиве" + }, + "noItemsInArchive": { + "message": "В архиве нет элементов" + }, + "noItemsInArchiveDesc": { + "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." + }, + "itemSentToArchive": { + "message": "Элемент отправлен в архив" + }, + "itemRemovedFromArchive": { + "message": "Элемент удален из архива" + }, + "archiveItem": { + "message": "Архивировать элемент" + }, + "archiveItemConfirmDesc": { + "message": "Архивированные элементы исключены из общих результатов поиска и предложений автозаполнения. Вы уверены, что хотите архивировать этот элемент?" } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index c283ca34fe1..8d4072e1da2 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 6e123bb38a1..566b9b8210a 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Upraviť skratku" + }, + "archive": { + "message": "Archivovať" + }, + "unarchive": { + "message": "Zrušiť archiváciu" + }, + "itemsInArchive": { + "message": "Položky v archíve" + }, + "noItemsInArchive": { + "message": "Žiadne položky v archíve" + }, + "noItemsInArchiveDesc": { + "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." + }, + "itemSentToArchive": { + "message": "Položka bola archivovaná" + }, + "itemRemovedFromArchive": { + "message": "Položka bola odobraná z archívu" + }, + "archiveItem": { + "message": "Archivovať položku" + }, + "archiveItemConfirmDesc": { + "message": "Archivované položky sú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania. Naozaj chcete archivovať túto položku?" } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 030167a6697..23a6df7ad95 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index a78483b9687..ca7cb763304 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1303,7 +1303,7 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Прикажи иконе веб локација и преузмите линкове промене лозинке" }, "enableMinToTray": { "message": "Минимизирај као иконицу у системској траци" @@ -3834,10 +3834,10 @@ "message": "Променити ризичну лозинку" }, "changeAtRiskPasswordAndAddWebsite": { - "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." + "message": "Ова пријава је ризична и недостаје веб локација. Додајте веб страницу и промените лозинку за јачу сигурност." }, "missingWebsite": { - "message": "Missing website" + "message": "Недостаје веб страница" }, "cannotRemoveViewOnlyCollections": { "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$", @@ -3935,10 +3935,10 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "aboutThisSetting": { - "message": "About this setting" + "message": "О овом подешавању" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden ће користити сачуване URI-јеве за пријаву да би одредио коју икону или URL за промену лозинке треба користити како би побољшао ваше искуство. Никакви подаци нису сакупљени нити сачувани приликом коришћења ове услуге." }, "assignToCollections": { "message": "Додели колекцијама" @@ -4100,12 +4100,39 @@ "message": "Потврди" }, "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "message": "Омогућава пречицу за аутоматски унос" }, "enableAutotypeDescriptionTransitionKey": { - "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." + "message": "Будите сигурни да сте у исправном пољу пре употребе пречице да бисте избегли попуњавање података на погрешно место." }, "editShortcut": { - "message": "Edit shortcut" + "message": "Уреди пречицу" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 701daee522f..e3cbba09d6b 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Redigera genväg" + }, + "archive": { + "message": "Arkivera" + }, + "unarchive": { + "message": "Packa upp" + }, + "itemsInArchive": { + "message": "Objekt i arkivet" + }, + "noItemsInArchive": { + "message": "Inga objekt i arkivet" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Objekt skickat till arkiv" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Arkivera objekt" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index 1364837b860..4874985a8fd 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "குறுக்குவழியைத் திருத்து" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 27394d39ece..5849d9d4cee 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index df2bdbad7b2..fa619695fdb 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index bb7f1219fcc..c33570af387 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Kısayolu düzenle" + }, + "archive": { + "message": "Arşivle" + }, + "unarchive": { + "message": "Arşivden çıkar" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 3adbb24e3c7..a9885566fd0 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 363b4647519..6613e983919 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Chỉnh sửa phím tắt" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 7b5a4b87e3b..102df7e9a27 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -897,7 +897,7 @@ "message": "您已登录!" }, "masterPassSent": { - "message": "我们已经为您发送了包含主密码提示的电子邮件。" + "message": "我们已经向您发送了一封包含主密码提示的电子邮件。" }, "unexpectedError": { "message": "发生意外错误。" @@ -1303,7 +1303,7 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "showIconsChangePasswordUrls": { - "message": "显示网站图标并检索更改密码的 URL" + "message": "显示网站图标并获取更改密码的 URL" }, "enableMinToTray": { "message": "最小化到托盘图标" @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "编辑快捷键" + }, + "archive": { + "message": "归档" + }, + "unarchive": { + "message": "取消归档" + }, + "itemsInArchive": { + "message": "归档中的项目" + }, + "noItemsInArchive": { + "message": "归档中没有项目" + }, + "noItemsInArchiveDesc": { + "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" + }, + "itemSentToArchive": { + "message": "项目已归档" + }, + "itemRemovedFromArchive": { + "message": "项目已取消归档" + }, + "archiveItem": { + "message": "归档项目" + }, + "archiveItemConfirmDesc": { + "message": "已归档的项目将被排除在一般搜索结果和自动填充建议之外。确定要归档此项目吗?" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index fa14858d62e..d4e579f89c1 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -1021,16 +1021,16 @@ "message": "選取兩步驟登入方式" }, "selfHostedEnvironment": { - "message": "自我裝載環境" + "message": "自行部署環境" }, "selfHostedBaseUrlHint": { - "message": "指定您自建的 Bitwarden 伺服器的網域 URL。例如:https://bitwarden.company.com" + "message": "指定您自架的 Bitwarden 伺服器的網域 URL。例如:https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { "message": "適用於進階設定。您可以單獨指定各個服務的網域 URL。" }, "selfHostedEnvFormInvalid": { - "message": "您必須新增伺服器網域 URL 或至少一個自定義環境。" + "message": "您必須新增伺服器網域 URL 或至少一個自訂環境。" }, "customEnvironment": { "message": "自訂環境" @@ -1045,7 +1045,7 @@ "message": "驗證工作階段因時間過久已逾時。請重試登入。" }, "selfHostBaseUrl": { - "message": "自建伺服器 URL", + "message": "自架伺服器 URL", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -4107,5 +4107,32 @@ }, "editShortcut": { "message": "Edit shortcut" + }, + "archive": { + "message": "Archive" + }, + "unarchive": { + "message": "Unarchive" + }, + "itemsInArchive": { + "message": "Items in archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "noItemsInArchiveDesc": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, + "itemSentToArchive": { + "message": "Item sent to archive" + }, + "itemRemovedFromArchive": { + "message": "Item removed from archive" + }, + "archiveItem": { + "message": "Archive item" + }, + "archiveItemConfirmDesc": { + "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" } } From f1a5d7af5e51142c67a7cc722ad6e16a5090ba1a Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:57:49 +0200 Subject: [PATCH 11/55] Autosync the updated translations (#16607) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 160 +++++++++++++ apps/web/src/locales/ar/messages.json | 160 +++++++++++++ apps/web/src/locales/az/messages.json | 160 +++++++++++++ apps/web/src/locales/be/messages.json | 160 +++++++++++++ apps/web/src/locales/bg/messages.json | 160 +++++++++++++ apps/web/src/locales/bn/messages.json | 160 +++++++++++++ apps/web/src/locales/bs/messages.json | 160 +++++++++++++ apps/web/src/locales/ca/messages.json | 160 +++++++++++++ apps/web/src/locales/cs/messages.json | 160 +++++++++++++ apps/web/src/locales/cy/messages.json | 160 +++++++++++++ apps/web/src/locales/da/messages.json | 160 +++++++++++++ apps/web/src/locales/de/messages.json | 160 +++++++++++++ apps/web/src/locales/el/messages.json | 160 +++++++++++++ apps/web/src/locales/en_GB/messages.json | 160 +++++++++++++ apps/web/src/locales/en_IN/messages.json | 160 +++++++++++++ apps/web/src/locales/eo/messages.json | 160 +++++++++++++ apps/web/src/locales/es/messages.json | 160 +++++++++++++ apps/web/src/locales/et/messages.json | 160 +++++++++++++ apps/web/src/locales/eu/messages.json | 160 +++++++++++++ apps/web/src/locales/fa/messages.json | 160 +++++++++++++ apps/web/src/locales/fi/messages.json | 160 +++++++++++++ apps/web/src/locales/fil/messages.json | 160 +++++++++++++ apps/web/src/locales/fr/messages.json | 162 ++++++++++++- apps/web/src/locales/gl/messages.json | 160 +++++++++++++ apps/web/src/locales/he/messages.json | 160 +++++++++++++ apps/web/src/locales/hi/messages.json | 160 +++++++++++++ apps/web/src/locales/hr/messages.json | 172 +++++++++++++- apps/web/src/locales/hu/messages.json | 160 +++++++++++++ apps/web/src/locales/id/messages.json | 160 +++++++++++++ apps/web/src/locales/it/messages.json | 178 +++++++++++++- apps/web/src/locales/ja/messages.json | 160 +++++++++++++ apps/web/src/locales/ka/messages.json | 160 +++++++++++++ apps/web/src/locales/km/messages.json | 160 +++++++++++++ apps/web/src/locales/kn/messages.json | 160 +++++++++++++ apps/web/src/locales/ko/messages.json | 160 +++++++++++++ apps/web/src/locales/lv/messages.json | 160 +++++++++++++ apps/web/src/locales/ml/messages.json | 160 +++++++++++++ apps/web/src/locales/mr/messages.json | 160 +++++++++++++ apps/web/src/locales/my/messages.json | 160 +++++++++++++ apps/web/src/locales/nb/messages.json | 160 +++++++++++++ apps/web/src/locales/ne/messages.json | 160 +++++++++++++ apps/web/src/locales/nl/messages.json | 160 +++++++++++++ apps/web/src/locales/nn/messages.json | 160 +++++++++++++ apps/web/src/locales/or/messages.json | 160 +++++++++++++ apps/web/src/locales/pl/messages.json | 160 +++++++++++++ apps/web/src/locales/pt_BR/messages.json | 160 +++++++++++++ apps/web/src/locales/pt_PT/messages.json | 162 ++++++++++++- apps/web/src/locales/ro/messages.json | 160 +++++++++++++ apps/web/src/locales/ru/messages.json | 160 +++++++++++++ apps/web/src/locales/si/messages.json | 160 +++++++++++++ apps/web/src/locales/sk/messages.json | 210 +++++++++++++++-- apps/web/src/locales/sl/messages.json | 160 +++++++++++++ apps/web/src/locales/sr_CS/messages.json | 160 +++++++++++++ apps/web/src/locales/sr_CY/messages.json | 160 +++++++++++++ apps/web/src/locales/sv/messages.json | 160 +++++++++++++ apps/web/src/locales/ta/messages.json | 160 +++++++++++++ apps/web/src/locales/te/messages.json | 160 +++++++++++++ apps/web/src/locales/th/messages.json | 160 +++++++++++++ apps/web/src/locales/tr/messages.json | 276 +++++++++++++++++----- apps/web/src/locales/uk/messages.json | 160 +++++++++++++ apps/web/src/locales/vi/messages.json | 160 +++++++++++++ apps/web/src/locales/zh_CN/messages.json | 280 ++++++++++++++++++----- apps/web/src/locales/zh_TW/messages.json | 190 +++++++++++++-- 63 files changed, 10255 insertions(+), 175 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index b4d73ccf0d2..fcfd90269f2 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Weens ’n ondernemingsbeleid mag u geen wagwoorde in u persoonlike kluis bewaar nie. Verander die eienaarskap na ’n organisasie en kies uit ’n van die beskikbare versamelings." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Deaktiveer Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO gedeaktiveer" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Geaktiveerde Key Connector" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index b17d503b3ad..fd0214fa71d 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index fc6db3b6695..8af382e2ad4 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Müəssisə siyasətinə görə, elementləri şəxsi seyfinizdə saxlamağınız məhdudlaşdırılıb. Sahiblik seçimini təşkilat olaraq dəyişdirin və mövcud kolleksiyalar arasından seçim edin." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "\"Send\"i sıradan çıxart" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO sıradan çıxarılıb" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector aktivdir" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Ödənişsiz təşkilatların ən çox 2 kolleksiyası ola bilər. Daha çox kolleksiya əlavə etmək üçün ödənişli bir plana yüksəldin." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Biznes vahidi" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "İndi doğrula." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 4c1337caa6b..91eb4328810 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "У адпаведнасці з палітыкай прадпрыемства вам забаронена захоўваць элементы ў асабістым сховішчы. Змяніце параметры ўласнасці на арганізацыю і выберыце з даступных калекцый." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Выдаліць Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO адключаны" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector уключаны" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 7d7ca9a49e7..c1519a74b36 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Заради някоя политика за голяма организация не може да запазвате елементи в собствения си трезор. Променете собствеността да е на организация и изберете от наличните колекции." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Без изпращане" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "Еднократното удостоверяване е изключено" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ трябва да се впише чрез еднократно удостоверяване", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Конекторът за ключове е включен" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Безплатните организации могат да имат не повече от 2 колекции. Надградете до платен план, ако искате да имате повече колекции." }, + "searchArchive": { + "message": "Търсене в архива" + }, + "archive": { + "message": "Архив" + }, + "noItemsInArchive": { + "message": "Няма елементи в архива" + }, + "archivedItemsDescription": { + "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." + }, "businessUnit": { "message": "Бизнес единица" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Потвърдете сега." + }, + "additionalStorageGB": { + "message": "Допълнително място в ГБ" + }, + "additionalServiceAccountsV2": { + "message": "Допълнителни машинни акаунти" + }, + "secretsManagerSeats": { + "message": "Брой потребители за Управлението на тайни" + }, + "additionalStorage": { + "message": "Допълнително място" + }, + "expandPurchaseDetails": { + "message": "Разгъване на подробностите за покупката" + }, + "collapsePurchaseDetails": { + "message": "Свиване на подробностите за покупката" + }, + "familiesMembership": { + "message": "Членство за семейства" + }, + "planDescPremium": { + "message": "Пълна сигурност в Интернет" + }, + "planDescFamiliesV2": { + "message": "Допълнителна защита за Вашето семейство" + }, + "planDescFreeV2": { + "message": "Споделяне с още $COUNT$ потребител", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Допълнителни възможности за всякакви организации" + }, + "planNameCustom": { + "message": "Персонализиран план" + }, + "planDescCustom": { + "message": "Битуорден работи за компании от всякакви размери, когато става въпрос за пароли и поверителна информация. Ако сте част от голяма компания, свържете се с отдела по продажби, за да попитате за цена." + }, + "builtInAuthenticator": { + "message": "Вграден удостоверител" + }, + "breachMonitoring": { + "message": "Наблюдение за пробиви" + }, + "andMoreFeatures": { + "message": "И още!" + }, + "secureFileStorage": { + "message": "Сигурно съхранение на файлове" + }, + "familiesUnlimitedSharing": { + "message": "Неограничено споделяне – изберете кой и какво може да вижда" + }, + "familiesUnlimitedCollections": { + "message": "Неограничен брой колекции за семейства" + }, + "familiesSharedStorage": { + "message": "Споделено пространство за важните семейни данни" + }, + "limitedUsersV2": { + "message": "До $COUNT$ членове", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "До $COUNT$ колекции", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Винаги безплатно" + }, + "twoSecretsIncluded": { + "message": "2 тайни" + }, + "projectsIncludedV2": { + "message": "$COUNT$ проект(а)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Сигурно споделяне на елементи" + }, + "scimSupport": { + "message": "Поддръжка на SCIM" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ машинни акаунти", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Политики за сигурност за големи компании" + }, + "selfHostOption": { + "message": "Възможност за собствен хостинг" + }, + "complimentaryFamiliesPlan": { + "message": "Допълнителен безплатен семеен план за всички потребители" + }, + "strengthenCybersecurity": { + "message": "Затвърдете киберсигурността" + }, + "boostProductivity": { + "message": "Увеличете продуктивността" + }, + "seamlessIntegration": { + "message": "Безпроблемна интеграция" } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 66f95891598..53f4cc91ae1 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 67fa94124a0..f53fc830b7a 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 01d1ea8a88f..d9dfa7c1eac 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "A causa d'una política empresarial, no podeu guardar elements a la vostra caixa forta personal. Canvieu l'opció Propietat en organització i trieu entre les col·leccions disponibles." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Suprimeix Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO inhabilitat" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Connector de claus habilitat" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Unitat de negoci" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index b414c0c3305..576fe8b4d3f 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Z důvodu podnikových zásad nemůžete ukládat položky do svého osobního trezoru. Změňte vlastnictví položky na organizaci a poté si vyberte z dostupných sbírek." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Odebrat Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO je vypnuto" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ se musí přihlásit jednotným přihlášením", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Je aktivován Key Connector" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Bezplatné organizace mohou mít až 2 sbírky. Chcete-li přidat více sbírek, přejděte na placený tarif." }, + "searchArchive": { + "message": "Hledat v archivu" + }, + "archive": { + "message": "Archivovat" + }, + "noItemsInArchive": { + "message": "Žádné položky v archivu" + }, + "archivedItemsDescription": { + "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." + }, "businessUnit": { "message": "Obchodní jednotka" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Ověřit nyní" + }, + "additionalStorageGB": { + "message": "Další úložiště (GB)" + }, + "additionalServiceAccountsV2": { + "message": "Další strojové účty" + }, + "secretsManagerSeats": { + "message": "Uživatelé Správce tajných klíčů" + }, + "additionalStorage": { + "message": "Další úložiště" + }, + "expandPurchaseDetails": { + "message": "Rozbalit podrobnosti o nákupu" + }, + "collapsePurchaseDetails": { + "message": "Sbalit podrobnosti o nákupu" + }, + "familiesMembership": { + "message": "Členství v rodinách" + }, + "planDescPremium": { + "message": "Dokončit online zabezpečení" + }, + "planDescFamiliesV2": { + "message": "Prémiové zabezpečení pro Vaši rodinu" + }, + "planDescFreeV2": { + "message": "Sdílet s dalšími $COUNT$ uživateli", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Pokročilé funkce pro jakékoli organizace" + }, + "planNameCustom": { + "message": "Vlastní plán" + }, + "planDescCustom": { + "message": "Bitwarden se přizpůsobí firmám všech velikostí a zajistí bezpečnost hesel a citlivých informací. Pokud jste součástí velké společnosti, kontaktujte obchodní oddělení a požádejte o cenovou nabídku." + }, + "builtInAuthenticator": { + "message": "Vestavěný autentifikátor" + }, + "breachMonitoring": { + "message": "Sledování úniků" + }, + "andMoreFeatures": { + "message": "A ještě více!" + }, + "secureFileStorage": { + "message": "Zabezpečené úložiště souborů" + }, + "familiesUnlimitedSharing": { + "message": "Neomezené sdílení - vyberte kdo co vidí" + }, + "familiesUnlimitedCollections": { + "message": "Neomezené rodinné sbírky" + }, + "familiesSharedStorage": { + "message": "Sdílené úložiště pro důležité rodinné informace" + }, + "limitedUsersV2": { + "message": "Pro až $COUNT$ členů", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Pro až $COUNT$ sbírek", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Vždy zdarma" + }, + "twoSecretsIncluded": { + "message": "2 tajné klíče" + }, + "projectsIncludedV2": { + "message": "$COUNT$ projektů", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Zabezpečené sdílení položek" + }, + "scimSupport": { + "message": "Podpora SCIM" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ strojových účtů", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Bezpečnosti zásady podniků" + }, + "selfHostOption": { + "message": "Volba vlastního hostitele" + }, + "complimentaryFamiliesPlan": { + "message": "Bezplatný rodinný plán pro všechny uživatele" + }, + "strengthenCybersecurity": { + "message": "Posílená kybernetická bezpečnost" + }, + "boostProductivity": { + "message": "Zvýšená produktivita" + }, + "seamlessIntegration": { + "message": "Hladká integrace" } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 7755341aae1..43fc75230a9 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 086fb3b7dac..f5441544b96 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Grundet en virksomhedspolitik kan du ikke gemme emner i din personlige boks. Skift ejerskabsindstillingen til en organisation, og vælg fra de tilgængelige samlinger." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Fjern Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO deaktiveret" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector aktiveret" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index f2186926cfd..012f562fe8c 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Aufgrund einer Unternehmensrichtlinie darfst du keine Einträge in deinem persönlichen Tresor speichern. Ändere die Eigentümer-Option in eine Organisation und wähle aus den verfügbaren Sammlungen." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Send entfernen" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO deaktiviert" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector aktiviert" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Kostenlose Organisationen können bis zu 2 Sammlungen haben. Upgrade auf ein kostenpflichtiges Abo, um mehr Sammlungen hinzuzufügen." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Geschäftsbereich" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 0f4695e0293..0c876ddd282 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Λόγω μιας Πολιτικής Επιχειρήσεων, δεν επιτρέπεται η αποθήκευση στοιχείων στο προσωπικό σας vault. Αλλάξτε την επιλογή Ιδιοκτησίας σε έναν οργανισμό και επιλέξτε από τις διαθέσιμες Συλλογές." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Απενεργοποίηση Αποστολής" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "Απενεργοποιημένο SSO" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Ενεργοποιημένος Σύνδεσμος Κλειδιών" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 867d4a483d6..edf4b59c5bd 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organisation and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned off" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organisations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organisation" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 4c079978ca4..ed092371092 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organization and choose from available Collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Disable Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organisations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organisation" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 8cc3eb5ade2..39ee969de3b 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Pro entreprena politiko, vi ne rajtas konservi artikolojn al via persona trezorejo. Ŝanĝu la opcion Proprieto al organizo kaj elektu el disponeblaj Kolektoj." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Neebligi la Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Aktiviĝis Key Connector" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 0d1e9586acd..7d76870ddc6 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Debido a una política empresarial, usted está restringido a guardar artículos en su caja fuerte personal. Cambie la opción Propiedad a una organización y elija de entre las colecciones disponibles." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Desactivar envío" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO desactivado" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Conector de claves habilitado" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 88382dff976..83ff0028e0d 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Ettevõtte poliitika tõttu ei saa sa andmeid oma personaalsesse Hoidlasse salvestada. Vali Omanikuks organisatsioon ja vali mõni saadavaolevatest Kogumikest." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Keela Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO välja lülitatud" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 4178d4f02b2..0dc74e0bb94 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Erakundeko politika bat dela eta, ezin dituzu elementuak zure kutxa gotor pertsonalean gorde. Aldatu jabe aukera erakunde aukera batera, eta aukeratu bilduma erabilgarrien artean." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Kendu Send-ak" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO desgaituta" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector gaituta" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 1b351197aad..0211b3a7d12 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "به دلیل سیاست پرمیوم، برای ذخیره موارد در گاوصندوق شخصی خود محدود شده اید. گزینه مالکیت را به یک سازمان تغییر دهید و مجموعه های موجود را انتخاب کنید." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "حذف ارسال" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO روشن شد" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "رابط کلید فعال شد" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "سازمان‌های رایگان می‌توانند حداکثر تا ۲ مجموعه داشته باشند. برای اضافه کردن مجموعه‌های بیشتر، به طرح پولی ارتقا دهید." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "واحد کسب و کار" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 6f84d9f3640..98f37b43cf4 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Yrityskäytännön johdosta kohteiden tallennus yksityiseen holviin ei ole mahdollista. Muuta omistusasetus organisaatiolle ja valitse käytettävissä olevista kokoelmista." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Poista Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "Kertakirjautuminen poistettiin käytöstä" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector otettiin käyttöön" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 201e84c42c4..8bb6d1fa53c 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Dahil sa Enterprise Policy, ikaw ay hindi pinapayagan na mag-save ng mga item sa iyong personal vault. Baguhin ang Ownership option sa isang organisasyon at pumili mula sa mga available na collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Alisin ang Ipadala" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "Nakabukas ang SSO" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Pinagana ang Key Connector" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 45d7410a44d..2f056f4e384 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "En raison d'une politique de sécurité Entreprise, il vous est interdit d'enregistrer des éléments dans votre coffre personnel. Sélectionnez une organisation dans l'option Propriété et choisissez parmi les collections disponibles." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Supprimer le Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO désactivé" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ doit se connecter avec une Authentification Unique (SSO)", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activé" }, @@ -10464,7 +10480,7 @@ "message": "Gestionnaire de Mots de Passe Bitwarden" }, "secretsManagerComplimentaryPasswordManager": { - "message": "Votre abonnement complémentairegratuit d'un an au Gestionnaire de Mots de Passe sera mis à niveau au plan sélectionné. Vous ne serez pas chargé avant la fin de votre période gratuite." + "message": "Votre abonnement offert gratuit d'un an au Gestionnaire de Mots de Passe sera mis à niveau au plan sélectionné. Vous ne serez pas chargé avant la fin de votre période gratuite." }, "fileSavedToDevice": { "message": "Fichier enregistré sur l'appareil. Gérez à partir des téléchargements de votre appareil." @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Les organisations gratuites peuvent avoir jusqu'à 2 collections. Passez à une offre payante pour ajouter plus de collections." }, + "searchArchive": { + "message": "Rechercher dans l'archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "Aucun élément dans l'archive" + }, + "archivedItemsDescription": { + "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." + }, "businessUnit": { "message": "Unité d'affaires" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Vérifier maintenant." + }, + "additionalStorageGB": { + "message": "Stockage additionnel (Go)" + }, + "additionalServiceAccountsV2": { + "message": "Comptes de machine supplémentaires" + }, + "secretsManagerSeats": { + "message": "Places du Secrets Manager" + }, + "additionalStorage": { + "message": "Stockage supplémentaire" + }, + "expandPurchaseDetails": { + "message": "Développer les détails de l'achat" + }, + "collapsePurchaseDetails": { + "message": "Réduire les détails de l'achat" + }, + "familiesMembership": { + "message": "Abonnement à Familles" + }, + "planDescPremium": { + "message": "Sécurité en ligne complète" + }, + "planDescFamiliesV2": { + "message": "Sécurité Premium pour votre famille" + }, + "planDescFreeV2": { + "message": "Partager avec $COUNT$ autres utilisateurs", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Fonctionnalités avancées pour toute organisation" + }, + "planNameCustom": { + "message": "Forfait personnalisé" + }, + "planDescCustom": { + "message": "Bitwarden évolue avec des entreprises de toutes tailles pour sécuriser les mots de passe et les informations sensibles. Si vous faites partie d'une grande entreprise, contactez les ventes pour demander une soumission." + }, + "builtInAuthenticator": { + "message": "Authentificateur intégré" + }, + "breachMonitoring": { + "message": "Surveillance des fuites" + }, + "andMoreFeatures": { + "message": "Et encore plus !" + }, + "secureFileStorage": { + "message": "Stockage sécurisé de fichier" + }, + "familiesUnlimitedSharing": { + "message": "Partage illimité - choisissez qui voit quoi" + }, + "familiesUnlimitedCollections": { + "message": "Collections familiales illimitées" + }, + "familiesSharedStorage": { + "message": "Stockage partagé pour les informations importantes de la famille" + }, + "limitedUsersV2": { + "message": "Jusqu'à $COUNT$ membres", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Jusqu'à $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Toujours gratuit" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ projet(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Partage sécurisé d’éléments" + }, + "scimSupport": { + "message": "Support SCIM" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ comptes machine", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Politiques de sécurité de l'entreprise" + }, + "selfHostOption": { + "message": "Option d'auto-hébergement" + }, + "complimentaryFamiliesPlan": { + "message": "Plan pour familles offert gratuitement pour tous les utilisateurs" + }, + "strengthenCybersecurity": { + "message": "Renforcer la cybersécurité" + }, + "boostProductivity": { + "message": "Améliore la productivité" + }, + "seamlessIntegration": { + "message": "Intégrations transparentes" } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 73a32f6bcc6..8c163ef4f6a 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 1fa6e8813e5..72a63ab210a 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "בשל מדיניות ארגונית, אתה מוגבל מלשמור פריטים לכספת האישית שלך. שנה את אפשרות הבעלות לארגון ובחר מאוספים זמינים." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "הסר סֵנְד" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO כבוי" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector הופעל" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "לארגונים חינמיים יכולים להיות עד 2 אוספים. שדרג לתוכנית בתשלום כדי להוסיף עוד אוספים." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "יחידת עסקים" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "אמת כעת." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 3cfddded149..9cdcabf7423 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 0fa944ecc32..66eca3f69a9 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -3,7 +3,7 @@ "message": "Sve aplikacije" }, "activity": { - "message": "Activity" + "message": "Aktivnost" }, "appLogoLabel": { "message": "Bitwarden logo" @@ -60,7 +60,7 @@ "message": "Stvori novu stavku prijave" }, "criticalApplicationsActivityDescription": { - "message": "Once you mark applications critical, they will display here." + "message": "Aplikacije označene kao kritične će biti prikazane ovdje." }, "criticalApplicationsWithCount": { "message": "Kritične aplikacije ($COUNT$)", @@ -72,7 +72,7 @@ } }, "countOfCriticalApplications": { - "message": "$COUNT$ critical applications", + "message": "Kritičnih aplikacija: $COUNT$", "placeholders": { "count": { "content": "$1", @@ -135,10 +135,10 @@ "message": "Rizični korisnici" }, "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "message": "Članovi koji mogu uređivati stavke za aplikacije označene kao kritične" }, "membersAtRisk": { - "message": "$COUNT$ members at risk", + "message": "Rizičnih članova: $COUNT$", "placeholders": { "count": { "content": "$1", @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Pravila tvrtke onemogućuju spremanje stavki u osobni trezor. Promijeni vlasništvo stavke na tvrtku i odaberi dostupnu Zbirku." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Onemogući Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO onemogućen" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ se mora prijavljivati sa SSO", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Konektor za ključ omogućen" }, @@ -9839,7 +9855,7 @@ "message": "Token nositelja" }, "repositoryNameHint": { - "message": "Name of the repository to ingest into" + "message": "Naziv repozitorija u koji se unosi" }, "index": { "message": "Indeks" @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Besplatne organizacije mogu imati do 2 zbirke. Nadogradi na plaćeni plan za dodavanje više zbirki." }, + "searchArchive": { + "message": "Pretraži arhivu" + }, + "archive": { + "message": "Arhiva" + }, + "noItemsInArchive": { + "message": "Nema stavki u arhivi" + }, + "archivedItemsDescription": { + "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." + }, "businessUnit": { "message": "Poslovna jedinica" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Potvrdi sada." + }, + "additionalStorageGB": { + "message": "Dodati GB pohrane" + }, + "additionalServiceAccountsV2": { + "message": "Dodatni mašinski računi" + }, + "secretsManagerSeats": { + "message": "Secret Manager licence" + }, + "additionalStorage": { + "message": "Dodatna pohrana" + }, + "expandPurchaseDetails": { + "message": "Proširi detalje kupnje" + }, + "collapsePurchaseDetails": { + "message": "Skupi detalje kupnje" + }, + "familiesMembership": { + "message": "Obiteljsko članstvo" + }, + "planDescPremium": { + "message": "Dovrši online sigurnost" + }, + "planDescFamiliesV2": { + "message": "Premium sigurnost za tvoju obitelj" + }, + "planDescFreeV2": { + "message": "Podijeli s ovoliko korisnika: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Napredne mogućnosti za bilo koju organizaciju" + }, + "planNameCustom": { + "message": "Vlastiti plan" + }, + "planDescCustom": { + "message": "Bitwarden se prilagođava tvrtkama svih veličina kako bi osigurao lozinke i osjetljive podatke. Ako si dio velike tvrtke, zatraži ponudu od odjela prodaje." + }, + "builtInAuthenticator": { + "message": "Ugrađeni autentifikator" + }, + "breachMonitoring": { + "message": "Nadzor proboja" + }, + "andMoreFeatures": { + "message": "I više!" + }, + "secureFileStorage": { + "message": "Sigurna pohrana datoteka" + }, + "familiesUnlimitedSharing": { + "message": "Neograničeno dijeljenje - odaberi tko vidi što" + }, + "familiesUnlimitedCollections": { + "message": "Neograničene obiteljske zbirke" + }, + "familiesSharedStorage": { + "message": "Dijeljena pohrana za važbe obiteljske informacije" + }, + "limitedUsersV2": { + "message": "Najveći broj članova: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Najveći broj zbirki: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Zauvijek besplatno" + }, + "twoSecretsIncluded": { + "message": "2 tajne" + }, + "projectsIncludedV2": { + "message": "Broj projekata: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Sigurno dijeljenje stavki" + }, + "scimSupport": { + "message": "SCIM podrška" + }, + "includedMachineAccountsV2": { + "message": "Mašinskih računa: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Sigurnosne politike tvrtke" + }, + "selfHostOption": { + "message": "Mogućnost samostalnog hostinga" + }, + "complimentaryFamiliesPlan": { + "message": "Besplatni obiteljski plan za sve korisnike" + }, + "strengthenCybersecurity": { + "message": "Jačanje kibernetičke sigurnosti" + }, + "boostProductivity": { + "message": "Povećanje produktivnosti" + }, + "seamlessIntegration": { + "message": "Jednostavna integracija" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 4a4b9b1e039..4a3ae2eb194 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Egy vállalati házirend miatt korlátozásra került az elemek személyes tárolóba történő mentése. Módosítsuk a Tulajdon opciót egy szervezetre és válasszunk az elérhető gyűjtemények közül." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Send letiltása" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "Az SSO bekapcsolásra került." }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ segítségével be kell jelentkezni egyszeri bejelentkezéssel.", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Bekapcsolt kulcskapcsoló" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Az ingyenes szervezeteknek legfeljebb 2 gyűjteményük lehet. Térjünk át egy fizetett csomagra további gyűjtemények hozzáadásához." }, + "searchArchive": { + "message": "Keresés archívum" + }, + "archive": { + "message": "Archívum" + }, + "noItemsInArchive": { + "message": "Nincs elem az archívumban." + }, + "archivedItemsDescription": { + "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." + }, "businessUnit": { "message": "Üzleti egység" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Ellenőrzés most" + }, + "additionalStorageGB": { + "message": "Kiegészítő tárhely (GB)" + }, + "additionalServiceAccountsV2": { + "message": "Kiegészítő gépi fiókok" + }, + "secretsManagerSeats": { + "message": "Titkos kód kezelő helyek" + }, + "additionalStorage": { + "message": "Kiegészítő tárhely" + }, + "expandPurchaseDetails": { + "message": "Vásárlás részletek kinyitása" + }, + "collapsePurchaseDetails": { + "message": "Vásárlás részletek összezárása" + }, + "familiesMembership": { + "message": "Családi tagság" + }, + "planDescPremium": { + "message": "Teljes körű online biztonság" + }, + "planDescFamiliesV2": { + "message": "Prémium biztonság a család számára" + }, + "planDescFreeV2": { + "message": "Megosztás $COUNT$ másik felhasználóval", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Fejlett képességek bármely szervezet számára" + }, + "planNameCustom": { + "message": "Egyedi csomag" + }, + "planDescCustom": { + "message": "A Bitwarden bármilyen méretű vállalkozásra kiterjed a jelszavak és érzékeny információk biztonsága érdekében. Ha egy nagyvállalat tagja vagyunk, lépjünk kapcsolatba az értékesítéssel ajánlat kéréshez." + }, + "builtInAuthenticator": { + "message": "Beépített hitelesítés" + }, + "breachMonitoring": { + "message": "Adatszivárgás figyelés" + }, + "andMoreFeatures": { + "message": "És sok más!" + }, + "secureFileStorage": { + "message": "Biztonságos fájl tárolás" + }, + "familiesUnlimitedSharing": { + "message": "Korlátlan megosztás - válasszuk ki, ki mit láthat" + }, + "familiesUnlimitedCollections": { + "message": "Korlátlan családi gyűjtemény" + }, + "familiesSharedStorage": { + "message": "Megosztott tárhely a fontos családi információkhoz" + }, + "limitedUsersV2": { + "message": "Legfeljebb $COUNT$ tag részére", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Legfeljebb $COUNT$ gyűjtemény", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Mindig ingyenes" + }, + "twoSecretsIncluded": { + "message": "2 titkos kód" + }, + "projectsIncludedV2": { + "message": "$COUNT$ projekt", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Biztonságos elem megosztás" + }, + "scimSupport": { + "message": "SCIM támogatás" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ gépi fiók", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Vállalati biztonsági irányelvek" + }, + "selfHostOption": { + "message": "Öntároló opció" + }, + "complimentaryFamiliesPlan": { + "message": "Kiegészítő családi csomag minden felhasználó számára" + }, + "strengthenCybersecurity": { + "message": "A kiberbiztonság megerősítése" + }, + "boostProductivity": { + "message": "Produktivitás növelése" + }, + "seamlessIntegration": { + "message": "Zökkenőmentes integráció" } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 2783d236264..39f4232a707 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Karena Kebijakan Perusahaan, Anda dilarang menyimpan item ke lemari besi pribadi Anda. Ubah opsi Kepemilikan ke organisasi dan pilih dari Koleksi yang tersedia." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Nonaktifkan Kirim" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO tidak aktif" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Konektor Kunci diaktifkan" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 7ed99e0ae29..3875bcb42f2 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -3,7 +3,7 @@ "message": "Tutte le applicazioni" }, "activity": { - "message": "Activity" + "message": "Attività" }, "appLogoLabel": { "message": "Logo Bitwarden" @@ -762,23 +762,23 @@ "message": "Visualizza elemento" }, "newItemHeaderLogin": { - "message": "New Login", + "message": "Nuovo Login", "description": "Header for new login item type" }, "newItemHeaderCard": { - "message": "New Card", + "message": "Nuova Carta", "description": "Header for new card item type" }, "newItemHeaderIdentity": { - "message": "New Identity", + "message": "Nuova Identità", "description": "Header for new identity item type" }, "newItemHeaderNote": { - "message": "New Note", + "message": "Nuova Nota", "description": "Header for new note item type" }, "newItemHeaderSshKey": { - "message": "New SSH key", + "message": "Nuova chiave SSH", "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { @@ -790,15 +790,15 @@ "description": "Header for new file send" }, "editItemHeaderLogin": { - "message": "Edit Login", + "message": "Modifica Login", "description": "Header for edit login item type" }, "editItemHeaderCard": { - "message": "Edit Card", + "message": "Modifica Carta", "description": "Header for edit card item type" }, "editItemHeaderIdentity": { - "message": "Edit Identity", + "message": "Modifica Identità", "description": "Header for edit identity item type" }, "editItemHeaderNote": { @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "A causa di una politica aziendale, non puoi salvare elementi nella tua cassaforte personale. Cambia l'opzione di proprietà in un'organizzazione e scegli tra le raccolte disponibili." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Rimuovi Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO disattivato" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector attivato" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Le organizzazioni gratuite possono avere fino a 2 raccolte. Aggiorna ad un piano a pagamento per crearne di più." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 5edd1b7a97b..3f2a2ada371 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "組織のポリシーにより、個人保管庫へのアイテムの保存が制限されています。 「所有権」オプションを組織に変更し、利用可能なコレクションから選択してください。" }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Send を無効化" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSOを無効にしました" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "キーコネクターを有効にしました" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "無料版の組織ではコレクションは 2 つまでです。さらにコレクションを追加するには有料プランにアップグレードしてください。" }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index d2ccbe5e3d5..170b989ff5d 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 6c6c71237bb..526b7567d99 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 739d82939e7..13f72bed6c0 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "ಎಂಟರ್‌ಪ್ರೈಸ್ ನೀತಿಯಿಂದಾಗಿ, ನಿಮ್ಮ ವೈಯಕ್ತಿಕ ವಾಲ್ಟ್‌ಗೆ ವಸ್ತುಗಳನ್ನು ಉಳಿಸುವುದರಿಂದ ನಿಮ್ಮನ್ನು ನಿರ್ಬಂಧಿಸಲಾಗಿದೆ. ಮಾಲೀಕತ್ವದ ಆಯ್ಕೆಯನ್ನು ಸಂಸ್ಥೆಗೆ ಬದಲಾಯಿಸಿ ಮತ್ತು ಲಭ್ಯವಿರುವ ಸಂಗ್ರಹಗಳಿಂದ ಆರಿಸಿಕೊಳ್ಳಿ." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "ಕಳುಹಿಸುವುದನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 89b8792a876..33ccaee2b5c 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "엔터프라이즈 정책으로 인해 개인 보관함에 항목을 저장할 수 없습니다. 조직에서 소유권 설정을 변경한 다음, 사용 가능한 컬렉션 중에서 선택해주세요." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Send 비활성화" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO 비활성화됨" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "키 커넥터 활성화됨" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index f57f0d92448..568e23d45b7 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Uzņēmuma nosacījumi liedz saglabāt vienumus privātajā glabātavā. Ir jānorāda piederība apvienībai un jāizvēlas kāds no pieejamajiem krājumiem." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Noņemt Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "Atspējota vienotā pieteikšanās" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ jāpiesakās ar vienoto pieteikšanos", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Iespējots Key Connector" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Apvienībās, kuras izmanto Bitwarden bez maksas, var būt līdz 2 krājumiem. Jāpāriet uz maksas plānu, lai pievienotu vairāk krājumu." }, + "searchArchive": { + "message": "Meklēt arhīvā" + }, + "archive": { + "message": "Arhivēt" + }, + "noItemsInArchive": { + "message": "Arhīvā nav vienumu" + }, + "archivedItemsDescription": { + "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." + }, "businessUnit": { "message": "Uzņēmējdarbības vienība" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Apliecini tagad!" + }, + "additionalStorageGB": { + "message": "Papildu krātuve GB" + }, + "additionalServiceAccountsV2": { + "message": "Papildu mašīnu konti" + }, + "secretsManagerSeats": { + "message": "Noslēpumu pārvaldnieka vietas" + }, + "additionalStorage": { + "message": "Papildu krātuve" + }, + "expandPurchaseDetails": { + "message": "Izvērst informāciju par pirkumu" + }, + "collapsePurchaseDetails": { + "message": "Sakļaut informāciju par pirkumu" + }, + "familiesMembership": { + "message": "Dalība ģimeņu plānā" + }, + "planDescPremium": { + "message": "Pilnīga drošība tiešsaistē" + }, + "planDescFamiliesV2": { + "message": "Augstākā labuma drošība ģimenei" + }, + "planDescFreeV2": { + "message": "Kopīgot ar $COUNT$ citiem lietotājiem", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Papildu spējas jebkurai apvienībai" + }, + "planNameCustom": { + "message": "Pielāgots plāns" + }, + "planDescCustom": { + "message": "Bitwarden piemērojas visu lielumu uzņēmējdarbībai, lai aizsargātu paroles un jūtīgu informāciju. Ja esi daļa no liela uzņēmuma, sazinies ar pārdošanas nodaļu, lai pieprasītu cenu piedāvājumu!" + }, + "builtInAuthenticator": { + "message": "Iebūvēts autentificētājs" + }, + "breachMonitoring": { + "message": "Noplūžu pārraudzīšana" + }, + "andMoreFeatures": { + "message": "Un vēl!" + }, + "secureFileStorage": { + "message": "Droša datņu krātuve" + }, + "familiesUnlimitedSharing": { + "message": "Neierobežota kopīgošana - izvēlies, ko kurš redz" + }, + "familiesUnlimitedCollections": { + "message": "Neierobežoti ģimeņu krājumi" + }, + "familiesSharedStorage": { + "message": "Koplietojama krātuve svarīgai ģimenes informācijai" + }, + "limitedUsersV2": { + "message": "Līdz $COUNT$ dalībniekiem", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Līdz $COUNT$ krājumiem", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Vienmēr bez maksas" + }, + "twoSecretsIncluded": { + "message": "2 noslēpumi" + }, + "projectsIncludedV2": { + "message": "$COUNT$ projekts(i)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Droša vienumu kopīgošana" + }, + "scimSupport": { + "message": "SCIM atbalsts" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ mašīnu konti", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Uzņēmumu drošības pamatnostādnes" + }, + "selfHostOption": { + "message": "Pašmitināšanas iespēja" + }, + "complimentaryFamiliesPlan": { + "message": "Bezmaksas ģimeņu plāns visiem lietotājiem" + }, + "strengthenCybersecurity": { + "message": "Stiprini kiberdrošību" + }, + "boostProductivity": { + "message": "Uzlabo ražīgumu" + }, + "seamlessIntegration": { + "message": "Plūdena iekļaušana" } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index a13420e9a7e..7f345a5b85c 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 99220e8a7ff..b25c6fb16ea 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 6c6c71237bb..526b7567d99 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 0e2b1bfecad..3cfcd87ec64 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "På grunn av en virksomhetsregel er du begrenset fra å lagre elementer til ditt personlige hvelv. Endre eierskapet til en organisasjon og velg blant tilgjengelige samlinger." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Deaktiver Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "Skrudde av SSO" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector aktivert" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 034f642dc7a..7dff2786a5d 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 275ec1e3c41..1ad5b20745a 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Wegens bedrijfsbeleid mag je geen wachtwoorden opslaan in je persoonlijke kluis. Verander het eigenaarschap naar een organisatie en kies uit een van de beschikbare collecties." }, + "desktopAutotypePolicy": { + "message": "Standaardinstelling Desktop Autotype" + }, + "desktopAutotypePolicyDesc": { + "message": "Desktop Autotype standaard inschakelen voor leden. Leden kunnen Autotype uitzetten in de Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Send uitschakelen" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO uitgeschakeld" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ moet met Single Sign-on inloggen", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector ingeschakeld" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Gratis organisaties kunnen maximaal twee collecties hebben. Upgrade naar een betaald abonnement voor het toevoegen van meer collecties." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Bedrijfseenheid" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Nu verifiëren." + }, + "additionalStorageGB": { + "message": "Extra opslagruimte (GB)" + }, + "additionalServiceAccountsV2": { + "message": "Extra machine-accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager-licenties" + }, + "additionalStorage": { + "message": "Extra opslagruimte" + }, + "expandPurchaseDetails": { + "message": "Aankoopgegevens uitklappen" + }, + "collapsePurchaseDetails": { + "message": "Aankoopgegevens inklappen" + }, + "familiesMembership": { + "message": "Lidmaatschap families" + }, + "planDescPremium": { + "message": "Online beveiliging voltooien" + }, + "planDescFamiliesV2": { + "message": "Premium beveiliging voor je familie" + }, + "planDescFreeV2": { + "message": "Delen met $COUNT$ andere gebruiker", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Geavanceerde mogelijkheden voor iedere bedrijf" + }, + "planNameCustom": { + "message": "Aangepast abonnement" + }, + "planDescCustom": { + "message": "Bitwarden schaalt met bedrijven van ieder formaat om wachtwoorden en gevoelige informatie te beveiligen. Als je deel uitmaakt van een grote onderneming, neem dan contact op met verkoop om een offerte aan te vragen." + }, + "builtInAuthenticator": { + "message": "Ingebouwde authenticator" + }, + "breachMonitoring": { + "message": "Lek-monitoring" + }, + "andMoreFeatures": { + "message": "En meer!" + }, + "secureFileStorage": { + "message": "Beveiligde bestandsopslag" + }, + "familiesUnlimitedSharing": { + "message": "Onbeperkt delen - kies wie wat ziet" + }, + "familiesUnlimitedCollections": { + "message": "Onbeperkte familiecollecties" + }, + "familiesSharedStorage": { + "message": "Gedeelde opslag voor belangrijke familie-informatie" + }, + "limitedUsersV2": { + "message": "Tot $COUNT$ leden", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Tot $COUNT$ collectie(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Altijd gratis" + }, + "twoSecretsIncluded": { + "message": "2 geheimen" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(en)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Beveiligd delen van items" + }, + "scimSupport": { + "message": "SCIM ondersteuning" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine-account(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise-beveiligingsbeleid" + }, + "selfHostOption": { + "message": "Optie tot zelf-hosten" + }, + "complimentaryFamiliesPlan": { + "message": "Gratis familie-abonnement voor alle gebruikers" + }, + "strengthenCybersecurity": { + "message": "Cyberbeveiliging versterken" + }, + "boostProductivity": { + "message": "Productiviteit verhogen" + }, + "seamlessIntegration": { + "message": "Naadloze integratie" } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index d99e1767eb0..54323d9b12a 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 6c6c71237bb..526b7567d99 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 5f6f15a8661..8254d28f142 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Ze względu na zasadę przedsiębiorstwa, nie możesz zapisywać elementów w osobistym sejfie. Zmień właściciela elementu na organizację i wybierz jedną z dostępnych kolekcji." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Usuń wysyłkę" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "Logowanie jednokrotne SSO zostało wyłączone" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Serwer Key Connector został włączony" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Darmowe organizacje mogą posiadać maksymalnie 2 kolekcje. Aby dodać więcej kolekcji, przejdź na plan płatny." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Jednostka Biznesowa" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index f8e4e695f4e..b2a2924b28b 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Devido a uma Política Empresarial, você está restrito de salvar itens para seu cofre pessoal. Altere a opção de propriedade para uma organização e escolha entre as Coleções disponíveis." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Desabilitar Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "Desativado o SSO" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Ativado o Conector de Chave" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Organizações gratuitas podem ter até duas coleções. Faça o upgrade para um plano pago para adicionar mais coleções." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Unidades de Negócio" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 3ccb06518c4..6f8e4d9c11c 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Devido a uma política empresarial, está impedido de guardar itens no seu cofre pessoal. Altere a opção Propriedade para uma organização e escolha entre as coleções disponíveis." }, + "desktopAutotypePolicy": { + "message": "Definição predefinida da digitação automática no computador" + }, + "desktopAutotypePolicyDesc": { + "message": "Ativar a digitação automática no computador por defeito para os membros. Os membros podem desativar manualmente a escrita automática no cliente Desktop.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remover Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO desativado" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ deve iniciar sessão com o início de sessão único", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector ativado" }, @@ -9839,7 +9855,7 @@ "message": "Token de portador" }, "repositoryNameHint": { - "message": "Name of the repository to ingest into" + "message": "Nome do repositório para ingestão" }, "index": { "message": "Índice" @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "As organizações gratuitas podem ter até 2 coleções. Atualize para um plano pago para adicionar mais coleções." }, + "searchArchive": { + "message": "Procurar no arquivo" + }, + "archive": { + "message": "Arquivar" + }, + "noItemsInArchive": { + "message": "Nenhum item no arquivo" + }, + "archivedItemsDescription": { + "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." + }, "businessUnit": { "message": "Unidade de negócio" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verificar agora." + }, + "additionalStorageGB": { + "message": "Armazenamento adicional (GB)" + }, + "additionalServiceAccountsV2": { + "message": "Contas automáticas adicionais" + }, + "secretsManagerSeats": { + "message": "Lugares do Gestor de Segredos" + }, + "additionalStorage": { + "message": "Armazenamento adicional" + }, + "expandPurchaseDetails": { + "message": "Expandir os detalhes da compra" + }, + "collapsePurchaseDetails": { + "message": "Recolher os detalhes da compra" + }, + "familiesMembership": { + "message": "Adesão familiar" + }, + "planDescPremium": { + "message": "Segurança total online" + }, + "planDescFamiliesV2": { + "message": "Segurança de topo para a sua família" + }, + "planDescFreeV2": { + "message": "Partilhar com $COUNT$ outro utilizador", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Capacidades avançadas para qualquer organização" + }, + "planNameCustom": { + "message": "Plano personalizado" + }, + "planDescCustom": { + "message": "O Bitwarden é dimensionado para empresas de todos os tamanhos para proteger palavras-passe e informações confidenciais. Se faz parte de uma grande empresa, contacte o departamento de vendas para solicitar um orçamento." + }, + "builtInAuthenticator": { + "message": "Autenticador incorporado" + }, + "breachMonitoring": { + "message": "Monitorização de violações" + }, + "andMoreFeatures": { + "message": "E muito mais!" + }, + "secureFileStorage": { + "message": "Armazenamento seguro de ficheiros" + }, + "familiesUnlimitedSharing": { + "message": "Partilha ilimitada - escolha quem vê o quê" + }, + "familiesUnlimitedCollections": { + "message": "Coleções familiares ilimitadas" + }, + "familiesSharedStorage": { + "message": "Armazenamento partilhado para informações importantes da família" + }, + "limitedUsersV2": { + "message": "Até $COUNT$ membros", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Até $COUNT$ coleções", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Sempre gratuito" + }, + "twoSecretsIncluded": { + "message": "2 segredos" + }, + "projectsIncludedV2": { + "message": "$COUNT$ projeto(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Partilha segura de itens" + }, + "scimSupport": { + "message": "Suporte SCIM" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ contas automáticas", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Políticas de segurança da empresa" + }, + "selfHostOption": { + "message": "Opção de auto-hospedagem" + }, + "complimentaryFamiliesPlan": { + "message": "Plano familiar gratuito para todos os utilizadores" + }, + "strengthenCybersecurity": { + "message": "Cibersegurança reforçada" + }, + "boostProductivity": { + "message": "Produtividade aumentada" + }, + "seamlessIntegration": { + "message": "Integração perfeita" } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 8ea49ad4472..364e7f86e64 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Din cauza unei politici de Întreprindere, nu puteți salva articole în seiful dvs. individual. Schimbați opțiunea de proprietate la o organizație și alegeți din colecțiile disponibile." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Înlăturare Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO a fost activat" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Conector cheie activat" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index b611e925c2e..753b5745f36 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "В соответствии с корпоративной политикой вам запрещено сохранять элементы в личном хранилище. Измените владельца на организацию и выберите из доступных Коллекций." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Отключить Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO отключен" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ должен авторизоваться с использованием SSO", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Соединитель ключей активирован" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "В бесплатных организациях может быть до 2 коллекций. Перейдите на платный план, чтобы добавить больше коллекций." }, + "searchArchive": { + "message": "Поиск в архиве" + }, + "archive": { + "message": "Архив" + }, + "noItemsInArchive": { + "message": "В архиве нет элементов" + }, + "archivedItemsDescription": { + "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." + }, "businessUnit": { "message": "Бизнес-единица" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Подтвердить сейчас." + }, + "additionalStorageGB": { + "message": "Дополнительные ГБ хранилища" + }, + "additionalServiceAccountsV2": { + "message": "Дополнительные аккаунты компьютеров" + }, + "secretsManagerSeats": { + "message": "Места менеджера секретов" + }, + "additionalStorage": { + "message": "Дополнительное хранилище" + }, + "expandPurchaseDetails": { + "message": "Отобразить информацию о покупке" + }, + "collapsePurchaseDetails": { + "message": "Скрыть информацию о покупке" + }, + "familiesMembership": { + "message": "Членство Families" + }, + "planDescPremium": { + "message": "Полная онлайн-защищенность" + }, + "planDescFamiliesV2": { + "message": "Премиальная защищенность \n для вашей семьи" + }, + "planDescFreeV2": { + "message": "Поделиться с другими пользователями: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Расширенные возможности для любой организации" + }, + "planNameCustom": { + "message": "Индивидуальный план" + }, + "planDescCustom": { + "message": "Bitwarden сотрудничает с компаниями любого размера, чтобы обеспечить защищенность паролей и конфиденциальной информации. Если вы являетесь частью крупного предприятия, обратитесь в отдел продаж, чтобы запросить ценовое предложение." + }, + "builtInAuthenticator": { + "message": "Встроенный аутентификатор" + }, + "breachMonitoring": { + "message": "Мониторинг нарушений" + }, + "andMoreFeatures": { + "message": "И многое другое!" + }, + "secureFileStorage": { + "message": "Защищенное хранилище файлов" + }, + "familiesUnlimitedSharing": { + "message": "Неограниченный доступ - выбирайте, кто и что видит" + }, + "familiesUnlimitedCollections": { + "message": "Неограниченные семейные коллекции" + }, + "familiesSharedStorage": { + "message": "Общее хранилище для важной семейной информации" + }, + "limitedUsersV2": { + "message": "До $COUNT$ участников", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "До $COUNT$ коллекций", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Всегда бесплатно" + }, + "twoSecretsIncluded": { + "message": "2 секрета" + }, + "projectsIncludedV2": { + "message": "$COUNT$ проектов", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Защищенный обмен элементами" + }, + "scimSupport": { + "message": "Поддержка SCIM" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ аккаунтов компьютеров", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Политики безопасности предприятия" + }, + "selfHostOption": { + "message": "Собственный хостинг" + }, + "complimentaryFamiliesPlan": { + "message": "Бесплатный семейный тариф для всех пользователей" + }, + "strengthenCybersecurity": { + "message": "Повышение кибербезопасности" + }, + "boostProductivity": { + "message": "Повышение производительности" + }, + "seamlessIntegration": { + "message": "Простая интеграция" } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 4c5b7eea1e9..b2b9714e766 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index c5e088c63a0..ab5864579d4 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -3,7 +3,7 @@ "message": "Všetky aplikácie" }, "activity": { - "message": "Activity" + "message": "Aktivita" }, "appLogoLabel": { "message": "Logo Bitwarden" @@ -60,7 +60,7 @@ "message": "Pridať novu položku s prihlásením" }, "criticalApplicationsActivityDescription": { - "message": "Once you mark applications critical, they will display here." + "message": "Tu sa zobrazia aplikácie, ktoré označíte za kritické." }, "criticalApplicationsWithCount": { "message": "Kritické aplikácie ($COUNT$)", @@ -72,7 +72,7 @@ } }, "countOfCriticalApplications": { - "message": "$COUNT$ critical applications", + "message": "$COUNT$ kritických aplikácií", "placeholders": { "count": { "content": "$1", @@ -135,10 +135,10 @@ "message": "Ohrozených členov" }, "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "message": "Členovia s oprávnením upravovať ohrozené položky kritických aplikácii" }, "membersAtRisk": { - "message": "$COUNT$ members at risk", + "message": "$COUNT$ ohrozených členov", "placeholders": { "count": { "content": "$1", @@ -762,79 +762,79 @@ "message": "Zobraziť položku" }, "newItemHeaderLogin": { - "message": "New Login", + "message": "Nové prihlásenie", "description": "Header for new login item type" }, "newItemHeaderCard": { - "message": "New Card", + "message": "Nová karta", "description": "Header for new card item type" }, "newItemHeaderIdentity": { - "message": "New Identity", + "message": "Nová identita", "description": "Header for new identity item type" }, "newItemHeaderNote": { - "message": "New Note", + "message": "Nová poznámka", "description": "Header for new note item type" }, "newItemHeaderSshKey": { - "message": "New SSH key", + "message": "Nový kľúč SSH", "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "New Text Send", + "message": "Nový textový Send", "description": "Header for new text send" }, "newItemHeaderFileSend": { - "message": "New File Send", + "message": "Nový súborový Send", "description": "Header for new file send" }, "editItemHeaderLogin": { - "message": "Edit Login", + "message": "Upraviť prihlásenie", "description": "Header for edit login item type" }, "editItemHeaderCard": { - "message": "Edit Card", + "message": "Upraviť kartu", "description": "Header for edit card item type" }, "editItemHeaderIdentity": { - "message": "Edit Identity", + "message": "Upraviť identitu", "description": "Header for edit identity item type" }, "editItemHeaderNote": { - "message": "Edit Note", + "message": "Upraviť poznámku", "description": "Header for edit note item type" }, "editItemHeaderSshKey": { - "message": "Edit SSH key", + "message": "Upraviť kľúč SSH", "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Edit Text Send", + "message": "Upraviť textový Send", "description": "Header for edit text send" }, "editItemHeaderFileSend": { - "message": "Edit File Send", + "message": "Upraviť súborový Send", "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "Zobraziť prihlásenie", "description": "Header for view login item type" }, "viewItemHeaderCard": { - "message": "View Card", + "message": "Zobraziť kartu", "description": "Header for view card item type" }, "viewItemHeaderIdentity": { - "message": "View Identity", + "message": "Zobraziť identitu", "description": "Header for view identity item type" }, "viewItemHeaderNote": { - "message": "View Note", + "message": "Zobraziť poznámku", "description": "Header for view note item type" }, "viewItemHeaderSshKey": { - "message": "View SSH key", + "message": "Zobraziť kľúč SSH", "description": "Header for view SSH key item type" }, "new": { @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Z dôvodu podnikovej politiky máte obmedzené ukladanie položiek do osobného trezora. Zmeňte možnosť vlastníctvo na organizáciu a vyberte si z dostupných zbierok." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Zakázať Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO zakázané" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ sa musí prihlásiť cez prihlasovací formulár spoločnosti ", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -9740,7 +9756,7 @@ "message": "Nepodarilo sa uložiť integráciu. Prosím skúste to neskôr." }, "failedToDeleteIntegration": { - "message": "Failed to delete integration. Please try again later." + "message": "Nepodarilo sa odstrániť integráciu. Prosím skúste to neskôr." }, "deviceIdMissing": { "message": "Chýba ID zariadenia" @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Bezplatné organizácie môžu mat maximálne dve zbierky. Ak chcete pridať viac zbierok povýšte na platené predplatné." }, + "searchArchive": { + "message": "Prehľadať archív" + }, + "archive": { + "message": "Archív" + }, + "noItemsInArchive": { + "message": "Žiadne položky v archíve" + }, + "archivedItemsDescription": { + "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." + }, "businessUnit": { "message": "Organizačná jednotka" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Overiť teraz." + }, + "additionalStorageGB": { + "message": "Dodatočné úložisko GB" + }, + "additionalServiceAccountsV2": { + "message": "Dodatočné strojové účty" + }, + "secretsManagerSeats": { + "message": "Sedenia pre Secrets Manager" + }, + "additionalStorage": { + "message": "Dodatočné úložisko" + }, + "expandPurchaseDetails": { + "message": "Rozbaliť detaily nákupu" + }, + "collapsePurchaseDetails": { + "message": "Zložiť detaily nákupu" + }, + "familiesMembership": { + "message": "Členstvo pre rodiny" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index dc5e3d76433..e54ef8bb4b6 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 14983e17bdc..8b8edaa1ec3 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index 128058dd092..4ad3b3723cf 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Због смерница за предузећа, ограничено вам је чување предмета у вашем личном трезору. Промените опцију власништва у организацију и изаберите из доступних колекција." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Онемогући слање" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO онемогућен" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Омогућити Key Connector" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Бесплатне организације могу имати до 2 колекције. Надоградите на плаћени план за додавање више колекција." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index c6efcd6ab38..6c5bb424a61 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "På grund av en av företagets policyer är du begränsad från att spara objekt till ditt personliga valv. Ändra ägarskap till en organisation och välj från tillgängliga samlingar." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Radera Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO inaktiverad" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Nyckelkontakt aktiverad" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Gratisorganisationer kan ha upp till 2 samlingar. Uppgradera till en betald plan för att lägga till fler samlingar." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Affärsenhet" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verifiera nu." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Ytterligare lagring" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Familjemedlemskap" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index c97b501a082..dc3e32726d4 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "எண்டர்பிரைஸ் கொள்கையின் காரணமாக, உங்கள் தனிப்பட்ட வால்ட்டில் பொருட்களைச் சேமிப்பதில் இருந்து நீங்கள் கட்டுப்படுத்தப்பட்டுள்ளீர்கள். உரிமை விருப்பத்தை ஒரு நிறுவனத்திற்கு மாற்றி, கிடைக்கக்கூடிய கலெக்‌ஷன்களிலிருந்து தேர்வு செய்யவும்." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Send-ஐ அகற்று" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO இயக்கப்பட்டது" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "கீ கனெக்டர் செயல்படுத்தப்பட்டது" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "இலவச நிறுவனங்களில் 2 சேகரிப்புகள் வரை இருக்கலாம். கூடுதல் சேகரிப்புகளைச் சேர்க்க கட்டணத் திட்டத்திற்கு மேம்படுத்தவும்." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "வணிகப் பிரிவு" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 6c6c71237bb..526b7567d99 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 628544f37af..0f9c3bea24d 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Remove Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO turned on" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector activated" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 874795b9f8b..f36a336a4d1 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -60,7 +60,7 @@ "message": "Yeni hesap kaydı oluştur" }, "criticalApplicationsActivityDescription": { - "message": "Once you mark applications critical, they will display here." + "message": "Uygulamaları kritik olarak işaretlediğinizde, bunlar burada görüntülenir." }, "criticalApplicationsWithCount": { "message": "Kritik uygulamalar ($COUNT$)", @@ -72,7 +72,7 @@ } }, "countOfCriticalApplications": { - "message": "$COUNT$ critical applications", + "message": "$COUNT$ kritik uygulama", "placeholders": { "count": { "content": "$1", @@ -135,10 +135,10 @@ "message": "Riskli üyeler" }, "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "message": "Kritik uygulamalar için risk altındaki kayıtlara düzenleme erişimi olan üyeler" }, "membersAtRisk": { - "message": "$COUNT$ members at risk", + "message": "$COUNT$ üye risk altında", "placeholders": { "count": { "content": "$1", @@ -450,7 +450,7 @@ "message": "Kimlik doğrulama anahtarı (TOTP)" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "2 adımlı doğrulamayı sorunsuz hale getirin" }, "totpHelper": { "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." @@ -1999,7 +1999,7 @@ "message": "Yeni bir cihazdan giriş yaptığınızda Bitwarden'ın size doğrulama e-postaları göndermesi için aşağıdan devam edin." }, "turnOffNewDeviceLoginProtectionWarning": { - "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + "message": "Yeni cihaz oturum açma koruması kapalıyken, ana parolanızı bilen herkes herhangi bir cihazdan hesabınıza erişebilir. Doğrulama e-postaları olmadan hesabınızı korumak için iki adımlı oturum açma özelliğini ayarlayın." }, "accountNewDeviceLoginProtectionSaved": { "message": "Yeni cihaz oturum açma koruması değişiklikleri kaydedildi" @@ -5180,13 +5180,13 @@ "message": "Üyelerin diğer kuruluşlara katılmasını kısıtlayın." }, "singleOrgPolicyDesc": { - "message": "Restrict members from joining other organizations. This policy is required for organizations that have enabled domain verification." + "message": "Üyelerin diğer kuruluşlara katılmasını kısıtlayın. Bu ilke, etki alanı doğrulamasını etkinleştirmiş kuruluşlar için gereklidir." }, "singleOrgBlockCreateMessage": { "message": "Mevcut kuruluşunuzun birden fazla kuruluşa katılmanıza izin vermeyen bir ilkesi var. Lütfen kuruluş yöneticilerinizle iletişime geçin veya farklı bir Bitwarden hesabı açın." }, "singleOrgPolicyMemberWarning": { - "message": "Non-compliant members will be placed in revoked status until they leave all other organizations. Administrators are exempt and can restore members once compliance is met." + "message": "Uygun olmayan üyeler, diğer tüm kuruluşlardan ayrılana kadar üyelikleri iptal edilmiş durumuna alınacaktır. Yöneticiler bu kuralın dışında tutulur ve uygunluk sağlandığında üyelerin üyeliklerini geri yükleyebilirler." }, "requireSso": { "message": "Çoklu oturum açma kimlik doğrulamasını zorunlu tut" @@ -5515,11 +5515,11 @@ "message": "Kuruluş veri sahipliğini zorunlu kılın" }, "organizationDataOwnershipDesc": { - "message": "Require all items to be owned by an organization, removing the option to store items at the account level.", + "message": "Tüm kayıtların bir kuruluşa ait olmasını zorunlu kılın ve kayıtları hesap düzeyinde depolama seçeneğini kaldırın.", "description": "This is the policy description shown in the policy list." }, "organizationDataOwnershipContent": { - "message": "All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection be available for each member to store items. Learn more about managing the ", + "message": "Tüm kayıtlar kuruluşun mülkiyetinde olacak ve kuruluşta saklanacak, böylece kuruluş genelinde kontrol, görünürlük ve raporlama sağlanacaktır. Etkinleştirildiğinde, her üyenin kayıtları depolaması için varsayılan bir koleksiyon kullanılabilir hale gelir. Yönetme hakkında daha fazla bilgi edinin ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection be available for each member to store items. Learn more about managing the credential lifecycle.'" }, "organizationDataOwnershipContentAnchor": { @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Bir kuruluş ilkesi nedeniyle kişisel kasanıza hesap kaydetmeniz kısıtlanmış. Sahip seçeneğini bir kuruluş olarak değiştirin ve mevcut koleksiyonlar arasından seçim yapın." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Send'i sil" }, @@ -6063,7 +6070,7 @@ "message": "Uyumlu olmayan üyeler" }, "nonCompliantMembersError": { - "message": "Members that are non-compliant with the Single organization or Two-step login policy cannot be restored until they adhere to the policy requirements" + "message": "Tek kuruluş veya İki aşamalı oturum açma ilkesine uymayan üyeler, ilke gerekliliklerine uymadıkları sürece geri yüklenemezler" }, "fingerprint": { "message": "Parmak izi" @@ -6247,7 +6254,7 @@ "message": "İzin verilen uygulamalar için kullanıcıların otomatik olarak oturum açması" }, "automaticAppLoginDesc": { - "message": "Login forms will automatically be filled and submitted for apps launched from your configured identity provider." + "message": "Hesap formları, yapılandırılmış kimlik sağlayıcınızdan başlatılan uygulamalar için otomatik olarak doldurulacak ve gönderilecektir." }, "automaticAppLoginIdpHostLabel": { "message": "Kimlik sağlayıcı ana bilgisayarı" @@ -6494,10 +6501,10 @@ "message": "Sponsorlu üye olmayan ailelerin planları burada görüntülenecektir" }, "sponsorshipFreeBitwardenFamilies": { - "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + "message": "Kuruluşunuzun üyeleri Ücretsiz Bitwarden Aileler'e hak kazanır. Bitwarden kuruluşunuzun üyesi olmayan çalışanlar için Ücretsiz Bitwarden Aileler'e sponsor olabilirsiniz. Üye olmayan bir kişiye sponsor olmak için kuruluşunuzda kullanılabilir bir yer olması gerekir." }, "sponsoredFamiliesRemoveActiveSponsorship": { - "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + "message": "Etkin bir sponsorluğu kaldırdığınızda, sponsorlu kuruluşun yenileme tarihinden sonra kuruluşunuzdaki bir yer boşalacaktır." }, "sponsoredFamiliesEligible": { "message": "Siz ve aileniz Ücretsiz Bitwarden Aileleri için uygunsunuz. Verilerinizi işte olmadığınızda bile güvende tutmak için kişisel e-postanızla kullanın." @@ -6524,7 +6531,7 @@ "message": "Aile planlarını kullanan üyeler burada görüntülenecektir" }, "membersWithSponsoredFamilies": { - "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + "message": "Kuruluşunuzun üyeleri, Ücretsiz Bitwarden Aileler'e hak kazanır. Burada, Aileler kuruluşuna sponsor olan üyeleri görebilirsiniz." }, "organizationHasMemberMessage": { "message": "Kuruluşunuzun bir üyesi olduğu için $EMAIL$ adresine sponsorluk gönderilemez.", @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO etkinleştirildi" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ çoklu oturum açma (SSO) ile giriş yapmalıdır", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector etkinleştirildi" }, @@ -6842,7 +6858,7 @@ "message": "Faturalandırma Eşitleme Anahtarı" }, "automaticBillingSyncDesc": { - "message": "Automatic sync unlocks Families sponsorships and allows you to sync your license without uploading a file. After making updates in the Bitwarden cloud server, select Sync License to apply changes." + "message": "Otomatik eşitleme, Aileler sponsorluklarını etkinleştirir ve dosya yüklemeden lisansınızı eşitlemenizi sağlar. Bitwarden bulut sunucusunda güncellemeleri yaptıktan sonra, değişiklikleri uygulamak için Lisansı Eşitle veya Lisansı Senkronize Et seçeneğini seçin." }, "active": { "message": "Aktif" @@ -8295,7 +8311,7 @@ "message": "Elle yükleme" }, "manualBillingTokenUploadDesc": { - "message": "If you do not want to opt into billing sync, manually upload your license here. This will not automatically unlock Families sponsorships." + "message": "Fatura eşitlemesini seçmek istemiyorsanız, lisansınızı buradan manuel olarak yükleyin. Bu, Aileler sponsorluklarını otomatik olarak açmaz." }, "syncLicense": { "message": "Lisansı eşitle" @@ -8367,7 +8383,7 @@ "message": "Yeni güvenlik önerilerini karşılamak ve hesap korumasını iyileştirmek için şifreleme ayarlarınızı güncelleyin." }, "kdfSettingsChangeLogoutWarning": { - "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." + "message": "Devam ettiğinizde tüm aktif oturumlardan çıkış yapacaksınız. Tekrar oturum açmanız ve varsa iki aşamalı oturum açma işlemini tamamlamanız gerekecektir. Veri kaybını önlemek için şifreleme ayarlarınızı değiştirmeden önce kasayı dışa aktarmanızı öneririz." }, "secretsManager": { "message": "Sır Yöneticisi" @@ -8779,7 +8795,7 @@ "message": "Erişimi etkinleştir" }, "bulkEnableSecretsManagerDescription": { - "message": "Grant the following members access to Secrets Manager. The role granted in the Password Manager will apply to Secrets Manager.", + "message": "Aşağıdaki üyelere Sır Yöneticisi için erişim izni verin. Parola Yöneticisi'nde verilen rol, Sır Yöneticisi'nde de uygulanacaktır.", "description": "This description is shown to an admin when they are attempting to add more users to Secrets Manager." }, "activateSecretsManager": { @@ -8943,7 +8959,7 @@ } }, "secretsManagerForPlanDesc": { - "message": "For engineering and DevOps teams to manage secrets throughout the software development lifecycle." + "message": "Mühendislik ve DevOps ekiplerinin yazılım geliştirme yaşam döngüsü boyunca sırları yönetmesi için." }, "free2PersonOrganization": { "message": "Ücretsiz 2 Kişilik Kuruluşlar" @@ -9430,7 +9446,7 @@ "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Aboneliğinizi sürdürmek için abonelik son kullanma tarihinden itibaren $DAYS$ gün süreli bir ödemesiz döneminiz vardır. Lütfen vadesi geçmiş faturaları $SUSPENSION_DATE$ tarihine kadar ödeyiniz.", "placeholders": { "days": { "content": "$1", @@ -9444,7 +9460,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Aboneliğinizi sürdürmek için, ilk ödenmemiş faturanızın vadesi geldiği tarihten itibaren $DAYS$ gün süreli bir ödemesiz döneminiz vardır. Lütfen vadesi geçmiş faturaları $SUSPENSION_DATE$ tarihine kadar ödeyiniz.", "placeholders": { "days": { "content": "$1", @@ -9639,7 +9655,7 @@ "message": "Sağlayıcıyı sil" }, "deleteProviderConfirmation": { - "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + "message": "Bir sağlayıcıyı silmek kalıcı ve geri alınamaz bir işlemdir. Sağlayıcıyı ve ilgili tüm verileri silmeyi onaylamak için ana parolanızı girin." }, "deleteProviderName": { "message": "$ID$ silinemedi", @@ -9712,20 +9728,20 @@ "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, "scimIntegrationDescEnd": { - "message": "(System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider.", + "message": "(Etki Alanları Arası Kimlik Yönetimi Sistemi (System for Cross-domain Identity Management (SCIM))) Kimlik Sağlayıcınızın uygulama kılavuzunu kullanarak kullanıcıları ve grupları Bitwarden'e otomatik olarak ekleyin.", "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, "bwdc": { "message": "Bitwarden Dizin Bağlayıcısı" }, "bwdcDesc": { - "message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider." + "message": "Kimlik Sağlayıcınızın uygulama kılavuzunu kullanarak kullanıcıları ve grupları otomatik olarak sağlamak için Bitwarden Dizin Bağlayıcısını yapılandırın." }, "eventManagement": { "message": "Olay yönetimi" }, "eventManagementDesc": { - "message": "Integrate Bitwarden event logs with your SIEM (system information and event management) system by using the implementation guide for your platform." + "message": "Platformunuz için uygulama kılavuzunu kullanarak Bitwarden olay günlüklerini SIEM (sistem bilgisi ve olay yönetimi) sisteminizle entegre edin." }, "deviceManagement": { "message": "Cihaz yönetimi" @@ -9839,7 +9855,7 @@ "message": "Taşıyıcı Erişim Anahtarı" }, "repositoryNameHint": { - "message": "Name of the repository to ingest into" + "message": "İçe aktarılacak deponun adı" }, "index": { "message": "İndeks" @@ -9996,7 +10012,7 @@ "message": "İzleme raporları ile güvenlik açıklarını kapatın" }, "upgradeOrganizationCloseSecurityGapsDesc": { - "message": "Stay ahead of security vulnerabilities by upgrading to a paid plan for enhanced monitoring." + "message": "Gelişmiş izleme için ücretli bir plana geçerek güvenlik açıklarının önüne geçin." }, "approveAllRequests": { "message": "Tüm istekleri onayla" @@ -10045,7 +10061,7 @@ "message": "Üye erişimini denetleyerek güvenlik risklerini belirleyin" }, "onlyAvailableForEnterpriseOrganization": { - "message": "Quickly view member access across the organization by upgrading to an Enterprise plan." + "message": "Kurumsal plana geçerek kuruluş genelinde üye erişimini hızlıca görüntüleyin." }, "date": { "message": "Tarih" @@ -10057,10 +10073,10 @@ "message": "Üye erişimi" }, "memberAccessReportDesc": { - "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." + "message": "Üyelerin doğru kimlik bilgilerine erişebildiğinden ve hesaplarının güvenli olduğundan emin olun. Bu raporu kullanarak üye erişimi ve hesap yapılandırmalarının CSV dosyasını elde edin." }, "memberAccessReportPageDesc": { - "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + "message": "Gruplar, koleksiyonlar ve koleksiyon kayıtları genelinde denetim kuruluşu üyelerinin erişimini denetleyin. CSV dışa aktarımı, koleksiyon izinleri ve hesap yapılandırmaları hakkında bilgiler dahil olmak üzere üye başına ayrıntılı bir döküm sağlar." }, "memberAccessReportNoCollection": { "message": "(Koleksiyon yok)" @@ -10156,13 +10172,13 @@ "message": "CSV'yi indir" }, "monthlySubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " + "message": "Aboneliğinizde yapılan değişiklikler, bir sonraki fatura döneminizde fatura toplamınıza orantılı olarak yansıtılacaktır. " }, "annualSubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges on a monthly billing cycle. " + "message": "Aboneliğinizde yapılan değişiklikler, aylık fatura döngüsünde orantılı olarak hesaplanan ücretlere neden olacaktır. " }, "billingHistoryDescription": { - "message": "Download a CSV to obtain client details for each billing date. Prorated charges are not included in the CSV and may vary from the linked invoice. For the most accurate billing details, refer to your monthly invoices.", + "message": "Her fatura tarihi için müşteri bilgilerini almak üzere bir CSV dosyası indirin. Orantılı ücretler CSV dosyasına dahil değildir ve bağlantılı faturadan farklılık gösterebilir. En doğru fatura bilgileri için aylık faturalarınıza bakın.", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." }, "noInvoicesToList": { @@ -10170,7 +10186,7 @@ "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." }, "providerClientVaultPrivacyNotification": { - "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "message": "Bildirim: Bu ayın sonlarında, istemci kasası gizliliği iyileştirilecek ve sağlayıcı üyeleri artık müşteri kasası kayıtlarına doğrudan erişemeyecektir. Sorularınız için,", "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." }, "contactBitwardenSupport": { @@ -10464,7 +10480,7 @@ "message": "Bitwarden Parola Yöneticisi" }, "secretsManagerComplimentaryPasswordManager": { - "message": "Your complimentary one year Password Manager subscription will upgrade to the selected plan. You will not be charged until the complimentary period is over." + "message": "Ücretsiz bir yıllık Parola Yöneticisi aboneliğiniz, seçtiğiniz plana yükseltilecektir. Ücretsiz süre sona erene kadar sizden herhangi bir ücret talep edilmeyecektir." }, "fileSavedToDevice": { "message": "Dosya cihaza kaydedildi. Cihazınızın indirilenler klasöründen yönetebilirsiniz." @@ -10539,7 +10555,7 @@ "description": "This represents the beginning of a sentence. The full sentence will be 'Manage subscription from the Provider Portal', but 'Provider Portal' will be a link and thus cannot be included in the translation file." }, "toHostBitwardenOnYourOwnServer": { - "message": "To host Bitwarden on your own server, you will need to upload your license file. To support Free Families plans and advanced billing capabilities for your self-hosted organization, you will need to set up automatic sync in your self-hosted organization." + "message": "Bitwarden'ı kendi sunucunuzda barındırmak için lisans dosyanızı yüklemeniz gerekir. Kendi sunucunuzda barındırılan kuruluşunuz için Ücretsiz Aileler planlarını ve gelişmiş faturalandırma özelliklerini desteklemek için, kendi sunucunuzda barındırılan kuruluşunuzda otomatik eşitlemeyi ayarlamanız gerekir." }, "selfHostingTitleProper": { "message": "Kendi Kendinize Barındırma" @@ -10548,7 +10564,7 @@ "message": "Bir alan adının talep edilmesi tek kuruluş ilkesini etkinleştirir." }, "single-org-revoked-user-warning": { - "message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations." + "message": "Uygun olmayan üyeler iptal edilecektir. Yöneticiler, üyeler diğer tüm kuruluşlardan ayrıldıktan sonra onları geri yükleyebilirler." }, "deleteOrganizationUser": { "message": "$NAME$ - sil", @@ -10635,10 +10651,10 @@ "message": "Üyelerin bu kuruluş aracılığıyla bir Aile planını kullanmalarına izin vermeyin." }, "verifyBankAccountWithStatementDescriptorWarning": { - "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the organization's billing page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + "message": "Banka hesabı ile ödeme yalnızca Amerika Birleşik Devletleri'ndeki müşteriler için geçerlidir. Banka hesabınızı doğrulamanız gerekecektir. Önümüzdeki 1-2 iş günü içinde mikro bir para yatırma işlemi gerçekleştireceğiz. Banka hesabını doğrulamak için kuruluşun fatura sayfasına bu para yatırma işleminin açıklama kodunu girin. Banka hesabının doğrulanmaması, ödemenin yapılmaması ve aboneliğinizin askıya alınmasıyla sonuçlanacaktır." }, "verifyBankAccountWithStatementDescriptorInstructions": { - "message": "We have made a micro-deposit to your bank account (this may take 1-2 business days). Enter the six-digit code starting with 'SM' found on the deposit description. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + "message": "Banka hesabınıza mikro bir para yatırımı yaptık (bu işlem 1-2 iş günü sürebilir). Para yatırma açıklamasında bulunan 'SM' ile başlayan altı haneli kodu girin. Banka hesabının doğrulanmaması, ödemenin yapılmaması ve aboneliğinizin askıya alınmasıyla sonuçlanacaktır." }, "descriptorCode": { "message": "Tanımlayıcı kodu" @@ -10680,7 +10696,7 @@ "message": "Otomatik Alınan Alan Adları" }, "automaticDomainClaimProcess": { - "message": "Bitwarden will attempt to claim the domain 3 times during the first 72 hours. If the domain can’t be claimed, check the DNS record in your host and manually claim. The domain will be removed from your organization in 7 days if it is not claimed." + "message": "Bitwarden, ilk 72 saat içinde etki alanını 3 kez talep etmeye çalışacaktır. Etki alanı talep edilemezse, barındırıcınızdaki DNS kaydını kontrol edin ve manuel olarak talep edin. Etki alanı talep edilmezse, 7 gün içinde kuruluşunuzdan kaldırılacaktır." }, "domainNotClaimed": { "message": "$DOMAIN$ alınmadı. DNS kayıtlarınızı kontrol edin.", @@ -10698,7 +10714,7 @@ "message": "Doğrulama altında" }, "claimedDomainsDescription": { - "message": "Claim a domain to own member accounts. The SSO identifier page will be skipped during login for members with claimed domains and administrators will be able to delete claimed accounts." + "message": "Üye hesaplarını sahip olmak için bir alan adı talep edin. Alan adı talep eden üyelerin oturum açma sırasında SSO tanımlayıcı sayfası atlanacak ve yöneticiler talep edilen hesapları silebilecek." }, "invalidDomainNameClaimMessage": { "message": "Giriş geçerli bir format değil. Biçim: mydomain.com. Alt alan adlarının alınması için ayrı girişler gerekir." @@ -10845,7 +10861,7 @@ "description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon [Bitwarden Icon] from the toolbar.'" }, "resellerRenewalWarningMsg": { - "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "message": "Aboneliğiniz yakında yenilenecektir. Kesintisiz hizmet için, $RENEWAL_DATE$ tarihinden önce $RESELLER$ ile iletişime geçerek yenileme işleminizi onaylayın.", "placeholders": { "reseller": { "content": "$1", @@ -10858,7 +10874,7 @@ } }, "resellerOpenInvoiceWarningMgs": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "message": "Aboneliğiniz için fatura $ISSUED_DATE$ tarihinde düzenlenmiştir. Hizmetin kesintisiz devam etmesi için, $DUE_DATE$ tarihinden önce $RESELLER$ ile iletişime geçerek yenileme işleminizi onaylatın.", "placeholders": { "reseller": { "content": "$1", @@ -10875,7 +10891,7 @@ } }, "resellerPastDueWarningMsg": { - "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "message": "Aboneliğinizin faturası ödenmemiştir. Hizmetin kesintisiz devam etmesi için, $RESELLER$ ile iletişime geçerek $GRACE_PERIOD_END$ tarihinden önce yenileme işleminizi onaylatın.", "placeholders": { "reseller": { "content": "$1", @@ -10903,10 +10919,10 @@ } }, "accountDeprovisioningNotification": { - "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + "message": "Yöneticiler artık talep edilen bir etki alanına ait üye hesaplarını silme yetkisine sahiptir." }, "deleteManagedUserWarningDesc": { - "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + "message": "Bu işlem, üye hesabını ve kasasındaki tüm kayıtları siler. Bu, önceki Kaldır işleminin yerini alır." }, "deleteManagedUserWarning": { "message": "Silme yeni bir eylemdir!" @@ -10970,7 +10986,7 @@ } }, "userkeyRotationDisclaimerDescription": { - "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + "message": "Şifreleme anahtarlarınızı değiştirmek için, hesabınızı kurtarabilecek tüm kuruluşların anahtarlarına ve acil durum erişimi etkinleştirdiğiniz tüm kişilere güvenmeniz gerekir. Devam etmek için aşağıdakileri doğruladığınızdan emin olun:" }, "userkeyRotationDisclaimerTitle": { "message": "Güvenilmeyen şifreleme anahtarları" @@ -10991,7 +11007,7 @@ "message": "Üyelerin hesaplarının kilidini PIN ile açmalarına izin vermeyin." }, "upgradeForFullEventsMessage": { - "message": "Event logs are not stored for your organization. Upgrade to a Teams or Enterprise plan to get full access to organization event logs." + "message": "Olay günlükleri kuruluşunuz için depolanmaz. Kuruluş olay günlüklerine tam erişim elde etmek için Ekip veya Kurumsal planına yükseltin." }, "upgradeEventLogTitleMessage": { "message": "Kuruluşunuzdaki olay günlüklerini görmek için yükseltin." @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Ücretsiz kuruluşların en fazla 2 koleksiyonu olabilir. Daha fazla koleksiyon eklemek için ücretli bir plana geçin." }, + "searchArchive": { + "message": "Arşivde ara" + }, + "archive": { + "message": "Arşiv" + }, + "noItemsInArchive": { + "message": "Arşivde kayıt yok" + }, + "archivedItemsDescription": { + "message": "Arşivlenen kayıtlar burada görünecek ve genel arama sonuçlarından ve otomatik doldurma önerilerinden hariç tutulacaktır." + }, "businessUnit": { "message": "İş Birimi" }, @@ -11137,13 +11165,13 @@ "message": "Yeniden başlat" }, "verifyProviderBankAccountWithStatementDescriptorWarning": { - "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + "message": "Banka hesabı ile ödeme yalnızca Amerika Birleşik Devletleri'ndeki müşteriler için geçerlidir. Banka hesabınızı doğrulamanız gerekecektir. Önümüzdeki 1-2 iş günü içinde mikro bir para yatırma işlemi gerçekleştireceğiz. Banka hesabını doğrulamak için, sağlayıcının abonelik sayfasına bu para yatırma işleminin ekstre tanımlama kodunu girin. Banka hesabının doğrulanmaması, ödemenin yapılmaması ve aboneliğinizin askıya alınmasıyla sonuçlanacaktır." }, "clickPayWithPayPal": { "message": "Ödeme yönteminizi eklemek için lütfen PayPal ile Öde düğmesine tıklayın." }, "revokeActiveSponsorshipConfirmation": { - "message": "If you remove $EMAIL$, the sponsorship for this Family plan will end. A seat within your organization will become available for members or sponsorships after the sponsored organization renewal date on $DATE$.", + "message": "$EMAIL$ adresini kaldırırsanız, bu Aile planının sponsorluğu sona erecektir. $DATE$ tarihinde sponsorlu kuruluşun yenileme tarihinden sonra, kuruluşunuzdaki bir yer üyeler veya sponsorluklar için kullanılabilir hale gelecektir.", "placeholders": { "email": { "content": "$1", @@ -11202,7 +11230,7 @@ "message": "CVV veya CVC olarak da bilinen kart güvenlik kodu, tipik olarak kredi kartınızın arkasında basılı 3 haneli bir sayı veya kart numaranızın üzerinde ön tarafta basılı 4 haneli bir sayıdır." }, "verifyBankAccountWarning": { - "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the Payment Details page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + "message": "Banka hesabı ile ödeme yalnızca Amerika Birleşik Devletleri'ndeki müşteriler için geçerlidir. Banka hesabınızı doğrulamanız gerekecektir. Önümüzdeki 1-2 iş günü içinde mikro bir para yatırma işlemi gerçekleştireceğiz. Banka hesabını doğrulamak için Ödeme Ayrıntıları sayfasına bu para yatırma işleminin ekstre tanımlama kodunu girin. Banka hesabının doğrulanmaması, ödemenin kaçırılmasına ve aboneliğinizin askıya alınmasına neden olacaktır." }, "taxId": { "message": "Vergi Kimliği: $TAX_ID$", @@ -11217,7 +11245,7 @@ "message": "Ödenmemiş faturalar" }, "unpaidInvoicesForServiceUser": { - "message": "Your subscription has not been paid. Contact your provider administrator to restore service to you and your clients.", + "message": "Aboneliğiniz ödenmemiştir. Hizmetin size ve müşterilerinize yeniden sağlanması için sağlayıcı yöneticinizle iletişime geçin.", "description": "A message shown in a non-dismissible dialog to service users of unpaid providers." }, "providerSuspended": { @@ -11230,11 +11258,11 @@ } }, "restoreProviderPortalAccessViaCustomerSupport": { - "message": "To restore access to your provider portal, contact Bitwarden Customer Support to renew your subscription.", + "message": "Sağlayıcı portalına erişiminizi geri yüklemek için Bitwarden Müşteri Desteği ile iletişime geçerek aboneliğinizi yenileyin.", "description": "A message shown in a non-dismissible dialog to any user of a suspended providers." }, "restoreProviderPortalAccessViaPaymentMethod": { - "message": "Your subscription has not been paid. To restore service to you and your clients, add a payment method by $CANCELLATION_DATE$.", + "message": "Aboneliğiniz ödenmemiştir. Size ve müşterilerinize hizmeti geri yüklemek için, $CANCELLATION_DATE$ tarihine kadar bir ödeme yöntemi ekleyin.", "placeholders": { "cancellation_date": { "content": "$1", @@ -11280,7 +11308,7 @@ "message": "Vergi Kimliği Eksik" }, "missingTaxIdWarning": { - "message": "Action required: You're missing a Tax ID number in payment details. If a Tax ID is not added, your invoices may include additional tax." + "message": "İşlem gerekli: Ödeme ayrıntılarında vergi kimlik numarası eksik. Vergi kimlik numarası eklenmezse, faturalarınızda ek vergi uygulanabilir." }, "moreBreadcrumbs": { "message": "Daha fazla gezinme izi", @@ -11330,12 +11358,144 @@ "message": "Key Connector alan adını doğrulayın" }, "requiredToVerifyBankAccountWithStripe": { - "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + "message": "Banka hesabı ile ödeme yalnızca Amerika Birleşik Devletleri'ndeki müşteriler için geçerlidir. Banka hesabınızı doğrulamanız gerekecektir. Önümüzdeki 1-2 iş günü içinde mikro bir para yatırma işlemi gerçekleştireceğiz. Banka hesabının doğrulanmaması, ödemenin kaçırılmasına ve aboneliğinizin askıya alınmasına neden olacaktır." }, "verifyBankAccountWithStripe": { - "message": "We have made a micro-deposit to your bank account. This may take 1-2 business days. When you see the deposit in your account, you can verify your bank account. Failure to verify your bank account will result in a missed payment and your subscription will be suspended." + "message": "Banka hesabınıza mikro bir para yatırımı yaptık. Bu işlem 1-2 iş günü sürebilir. Hesabınızda para yatırımı gördüğünüzde, banka hesabınızı doğrulayabilirsiniz. Banka hesabınızı doğrulamamanız durumunda ödeme yapılmayacak ve aboneliğiniz askıya alınacaktır." }, "verifyNow": { "message": "Şimdi doğrulayın." + }, + "additionalStorageGB": { + "message": "Ek depolama alanı GB" + }, + "additionalServiceAccountsV2": { + "message": "İlave makine hesapları" + }, + "secretsManagerSeats": { + "message": "Sır Yöneticisi yerleri" + }, + "additionalStorage": { + "message": "Ek depolama" + }, + "expandPurchaseDetails": { + "message": "Satın alma ayrıntılarını genişlet" + }, + "collapsePurchaseDetails": { + "message": "Satın alma ayrıntılarını daralt" + }, + "familiesMembership": { + "message": "Aile üyeliği" + }, + "planDescPremium": { + "message": "Tam çevrimiçi güvenlik" + }, + "planDescFamiliesV2": { + "message": "Aileniz için Premium güvenlik" + }, + "planDescFreeV2": { + "message": "$COUNT$ diğer kullanıcıyla paylaş", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Herhangi bir kuruluş için gelişmiş yetenekler" + }, + "planNameCustom": { + "message": "Özel plan" + }, + "planDescCustom": { + "message": "Bitwarden, her büyüklükteki işletmeyle uyumlu olarak parolaları ve hassas bilgileri güvence altına alır. Büyük kurumsal bir işletmenin parçasıysanız, satış ekibiyle iletişime geçerek fiyat teklifi isteyin." + }, + "builtInAuthenticator": { + "message": "Dahili kimlik doğrulayıcı" + }, + "breachMonitoring": { + "message": "İhlal izleme" + }, + "andMoreFeatures": { + "message": "Ve daha fazlası!" + }, + "secureFileStorage": { + "message": "Güvenli dosya depolama" + }, + "familiesUnlimitedSharing": { + "message": "Sınırsız paylaşım - kimin neyi göreceğini seçin" + }, + "familiesUnlimitedCollections": { + "message": "Sınırsız aile koleksiyonları" + }, + "familiesSharedStorage": { + "message": "Önemli aile bilgileri için paylaşılmış depolama" + }, + "limitedUsersV2": { + "message": "$COUNT$ üyeye kadar", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "$COUNT$ koleksiyona kadar", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Her zaman ücretsiz" + }, + "twoSecretsIncluded": { + "message": "2 sır" + }, + "projectsIncludedV2": { + "message": "$COUNT$ proje", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Güvenli kayıt paylaşımı" + }, + "scimSupport": { + "message": "SCIM desteği" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ makine hesabı", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Kurumsal güvenlik ilkeleri" + }, + "selfHostOption": { + "message": "Kendi sunucunuzda barındırma seçeneği" + }, + "complimentaryFamiliesPlan": { + "message": "Tüm kullanıcılar için ücretsiz aile planı" + }, + "strengthenCybersecurity": { + "message": "Siber güvenliği güçlendirin" + }, + "boostProductivity": { + "message": "Verimliliği artırın" + }, + "seamlessIntegration": { + "message": "Sorunsuz entegrasyon" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index df20e2db5ee..c79e6460c33 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "У зв'язку з політикою компанії, вам не дозволено зберігати записи до особистого сховища. Змініть налаштування власності на організацію та виберіть серед доступних збірок." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Вилучити відправлення" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO вимкнено" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector увімкнено" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Безплатні організації можуть мати до 2 збірок. Передплатіть тарифний план, щоб додати більше збірок." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Бізнес-підрозділ" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 4367d3a04eb..155a08a76be 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "Do chính sách của doanh nghiệp, bạn không thể lưu trữ các mục vào kho cá nhân của mình. Hãy thay đổi tùy chọn Quyền sở hữu thành tổ chức và chọn từ các bộ sưu tập có sẵn." }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "Xóa Send" }, @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO đã được bật" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector đã được kích hoạt" }, @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Các tổ chức miễn phí có thể có tối đa 2 bộ sưu tập. Nâng cấp lên gói trả phí để thêm nhiều bộ sưu tập hơn." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Bộ phận kinh doanh" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Xác minh ngay." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 6ff2caaa5d9..423290706e0 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -1432,7 +1432,7 @@ "message": "账户创建成功。" }, "masterPassSent": { - "message": "我们已经为您发送了包含主密码提示的电子邮件。" + "message": "我们已经向您发送了一封包含主密码提示的电子邮件。" }, "unexpectedError": { "message": "发生意外错误。" @@ -2188,7 +2188,7 @@ "message": "更改网页密码库的语言。" }, "showIconsChangePasswordUrls": { - "message": "显示网站图标并检索更改密码的 URL" + "message": "显示网站图标并获取更改密码的 URL" }, "default": { "message": "默认" @@ -2579,7 +2579,7 @@ "message": "未激活两步登录" }, "inactive2faReportDesc": { - "message": "两步登录为您的账户增加了一层保护。使用 Bitwarden 验证器或其他方式为这些账户开启两步登录。" + "message": "两步登录为您的账户增加了一层保护。使用 Bitwarden Authenticator 或其他方式为这些账户开启两步登录。" }, "inactive2faFound": { "message": "发现未启用两步登录的登录项目" @@ -2771,7 +2771,7 @@ "message": "计费" }, "billingPlanLabel": { - "message": "计费计划" + "message": "计费方案" }, "paymentType": { "message": "付款类型" @@ -2855,7 +2855,7 @@ } }, "bitwardenFamiliesPlan": { - "message": "Bitwarden 家庭计划。" + "message": "Bitwarden 家庭方案。" }, "addons": { "message": "附加项目" @@ -2883,7 +2883,7 @@ "message": "# GB 附加存储" }, "additionalStorageIntervalDesc": { - "message": "您的计划包含 $SIZE$ 的加密存储空间。您也可以以每 GB $PRICE$ /$INTERVAL$ 购买附加存储。", + "message": "您的方案包含 $SIZE$ 加密文件存储空间。您也可以以每 GB $PRICE$ /$INTERVAL$ 购买附加存储。", "placeholders": { "size": { "content": "$1", @@ -2940,7 +2940,7 @@ "message": "任何未付费订阅都将通过您的付款方式收取费用。" }, "paymentChargedWithTrial": { - "message": "您的计划包含了 7 天的免费试用。在试用期结束前,不会从您的付款方式中扣款。您可以随时取消。" + "message": "您的方案包含了 7 天的免费试用。在试用期结束前,不会从您的付款方式中扣款。您可以随时取消。" }, "paymentInformation": { "message": "支付信息" @@ -3162,7 +3162,7 @@ "message": "公司名称" }, "chooseYourPlan": { - "message": "选择您的计划" + "message": "选择您的方案" }, "users": { "message": "用户" @@ -3177,7 +3177,7 @@ "message": "# 用户席位" }, "userSeatsAdditionalDesc": { - "message": "您的计划包含 $BASE_SEATS$ 个用户席位。您也可以以每用户 $SEAT_PRICE$ /月购买附加用户。", + "message": "您的方案包含 $BASE_SEATS$ 个用户席位。您也可以以每用户 $SEAT_PRICE$ /月购买附加用户。", "placeholders": { "base_seats": { "content": "$1", @@ -3193,7 +3193,7 @@ "message": "您需要多少个用户席位?您也可以在以后需要的时候添加附加席位。" }, "planNameFree": { - "message": "免费", + "message": "免费版", "description": "Free as in 'free beer'." }, "planDescFree": { @@ -3206,7 +3206,7 @@ } }, "planNameFamilies": { - "message": "家庭" + "message": "家庭版" }, "planDescFamilies": { "message": "适用于个人使用,与家人和朋友共享。" @@ -3323,7 +3323,7 @@ } }, "trialThankYou": { - "message": "感谢您注册适用于 $PLAN$ 的 Bitwarden!", + "message": "感谢您注册 $PLAN$ Bitwarden!", "placeholders": { "plan": { "content": "$1", @@ -3332,7 +3332,7 @@ } }, "trialSecretsManagerThankYou": { - "message": "感谢您注册适用于 $PLAN$ 的 Bitwarden 机密管理器!", + "message": "感谢您注册 $PLAN$ Bitwarden 机密管理器!", "placeholders": { "plan": { "content": "$1", @@ -4382,15 +4382,15 @@ "message": "对于美国境内的客户,需要提供邮政编码以满足销售税要求。对于其他国家,您可以选择提供一个税号 (VAT/GST) 和/或地址来显示在您的账单上。" }, "billingPlan": { - "message": "计划", + "message": "方案", "description": "A billing plan/package. For example: Families, Teams, Enterprise, etc." }, "changeBillingPlan": { - "message": "升级计划", + "message": "升级方案", "description": "A billing plan/package. For example: Families, Teams, Enterprise, etc." }, "changeBillingPlanUpgrade": { - "message": "要将您的账户升级到另一个计划,请提供以下信息。同时请确保您账户已添加有一个有效的付款方式。", + "message": "要将您的账户升级到另一个方案,请提供以下信息。同时请确保您账户已添加有一个有效的付款方式。", "description": "A billing plan/package. For example: Families, Teams, Enterprise, etc." }, "invoiceNumber": { @@ -4529,7 +4529,7 @@ } }, "subscriptionUserSeatsWithoutAdditionalSeatsOption": { - "message": "您最多可邀请 $COUNT$ 名成员,而无需额外付费。要升级您的计划并邀请更多成员,请联系客户支持。", + "message": "您最多可邀请 $COUNT$ 名成员,而无需额外付费。要升级您的方案并邀请更多成员,请联系客户支持。", "placeholders": { "count": { "content": "$1", @@ -4538,7 +4538,7 @@ } }, "subscriptionFreePlan": { - "message": "如果不升级您的计划,您最多只能邀请 $COUNT$ 位成员。", + "message": "如果不升级您的方案,您最多只能邀请 $COUNT$ 位成员。", "placeholders": { "count": { "content": "$1", @@ -4547,7 +4547,7 @@ } }, "subscriptionUpgrade": { - "message": "如果不升级您的计划,您最多只能邀请 $COUNT$ 位成员。", + "message": "如果不升级您的方案,您最多只能邀请 $COUNT$ 位成员。", "placeholders": { "count": { "content": "$1", @@ -4556,7 +4556,7 @@ } }, "subscriptionSponsoredFamiliesPlan": { - "message": "您的订阅一共允许 $COUNT$ 位成员。您的计划由一个外部组织赞助和支付费用。", + "message": "您的订阅一共允许 $COUNT$ 位成员。您的方案由一个外部组织赞助和支付费用。", "placeholders": { "count": { "content": "$1", @@ -4677,7 +4677,7 @@ "message": "升级组织" }, "upgradeOrganizationDesc": { - "message": "此功能不适用于免费组织。请切换到付费计划以解锁更多功能。" + "message": "此功能不适用于免费组织。请切换到付费方案以解锁更多功能。" }, "createOrganizationStep1": { "message": "创建组织:第一步" @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "由于某个企业策略,您不能将项目保存到您的个人密码库。请将所有权选项更改为组织,然后选择可用的集合。" }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "禁用 Send" }, @@ -5600,7 +5607,7 @@ } }, "planPrice": { - "message": "计划价格" + "message": "方案价格" }, "estimatedTax": { "message": "预估税额" @@ -5624,7 +5631,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Custom roles is an enterprise feature. Contact our support team to upgrade your subscription'" }, "customNonEnterpriseError": { - "message": "要启用自定义权限,该组织必须处于 2020 企业计划中。" + "message": "要启用自定义权限,该组织必须处于 2020 企业方案中。" }, "permissions": { "message": "权限" @@ -5736,10 +5743,10 @@ "message": "使用端到端加密保护机密。而不再需要硬编码机密或通过 .env 文件分享机密。" }, "enhanceDeveloperProductivity": { - "message": "提高开发人员的生产力。" + "message": "提升开发人员的工作效率。" }, "enhanceDeveloperProductivityDescription": { - "message": "程序化地在运行时检索和部署机密,使开发人员可以专注于最重要的事情,例如提高代码质量。" + "message": "程序化地在运行时获取和部署机密,使开发人员可以专注于最重要的事情,例如提高代码质量。" }, "strengthenBusinessSecurity": { "message": "加强企业安全。" @@ -6491,7 +6498,7 @@ "message": "没有赞助的家庭" }, "nosponsoredFamiliesDetails": { - "message": "已赞助的非成员家庭计划将显示在这里" + "message": "已赞助的非成员家庭方案将显示在这里" }, "sponsorshipFreeBitwardenFamilies": { "message": "您的组织成员有资格获得免费的 Bitwarden 家庭计划。您可以为不是您的 Bitwarden 组织成员的员工赞助免费 Bitwarden 家庭。赞助非成员需要您的组织内有可用的席位。" @@ -6503,16 +6510,16 @@ "message": "您和您的家人有资格获得免费的 Bitwarden 家庭版计划。使用您的个人电子邮箱兑换,即使您不在工作中,也能确保您的数据安全。" }, "sponsoredFamiliesEligibleCard": { - "message": "立即兑换免费的 Bitwarden 家庭计划,即使您不在工作中,也能确保您的数据安全。" + "message": "立即兑换免费的 Bitwarden 家庭方案,即使您不在工作中,也能确保您的数据安全。" }, "sponsoredFamiliesIncludeMessage": { - "message": "Bitwarden 家庭计划包含" + "message": "Bitwarden 家庭方案包含" }, "sponsoredFamiliesPremiumAccess": { "message": "最多 6 个用户的高级访问权限" }, "sponsoredFamiliesSharedCollectionsForFamilyMembers": { - "message": "适用于家庭成员的共享集合" + "message": "为家庭成员提供共享集合" }, "memberFamilies": { "message": "成员家庭" @@ -6521,7 +6528,7 @@ "message": "没有成员家庭" }, "noMemberFamiliesDescription": { - "message": "已兑换家庭计划的成员将在这里显示" + "message": "已兑换家庭方案的成员将在这里显示" }, "membersWithSponsoredFamilies": { "message": "您的组织成员有资格获得免费的 Bitwarden 家庭计划。在这里,您可以看到已赞助了家庭组织的成员。" @@ -6539,7 +6546,7 @@ "message": "链接已失效。请让赞助方重新发送邀请。" }, "reclaimedFreePlan": { - "message": "收回了免费计划" + "message": "收回了免费方案" }, "redeem": { "message": "兑换" @@ -6560,7 +6567,7 @@ "message": "接受现有组织的邀请或创建一个新的家庭组织。" }, "setupSponsoredFamiliesLoginDesc": { - "message": "您已被邀请加入免费的 Bitwarden 家庭计划组织。要继续,您需要登录到接收邀请的账户。" + "message": "您已被邀请加入免费的 Bitwarden 家庭方案组织。要继续,您需要登录到接收邀请的账户。" }, "sponsoredFamiliesAcceptFailed": { "message": "无法接受邀请。请通过您的企业账户重新发送邀请邮件,然后重试。" @@ -6605,7 +6612,7 @@ } }, "freeFamiliesPlan": { - "message": "免费家庭计划" + "message": "免费家庭方案" }, "redeemNow": { "message": "立即兑换" @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "SSO 已关闭" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ 必须使用单点登录", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "Key Connector 已启用" }, @@ -6830,7 +6846,7 @@ "message": "自托管" }, "selfHostingEnterpriseOrganizationSectionCopy": { - "message": "要在您自己的服务器上设置您的组织,您需要上传您的许可证文件。要为您的自托管组织提供免费家庭计划和高级计费功能,您需要设置计费同步。" + "message": "要在您自己的服务器上设置您的组织,您需要上传您的许可证文件。要为您的自托管组织提供免费家庭方案和高级计费功能,您需要设置计费同步。" }, "billingSyncApiKeyRotated": { "message": "令牌已轮换" @@ -8190,7 +8206,7 @@ "message": "切换产品" }, "freeOrgInvLimitReachedManageBilling": { - "message": "免费组织最多拥有 $SEATCOUNT$ 位成员。要邀请更多成员,请升级到付费计划。", + "message": "免费组织最多拥有 $SEATCOUNT$ 位成员。要邀请更多成员,请升级到付费方案。", "placeholders": { "seatcount": { "content": "$1", @@ -8208,7 +8224,7 @@ } }, "teamsStarterPlanInvLimitReachedManageBilling": { - "message": "团队入门版计划最多拥有 $SEATCOUNT$ 位成员。要邀请更多成员,请升级您的计划。", + "message": "团队入门版方案最多拥有 $SEATCOUNT$ 位成员。要邀请更多成员,请升级您的方案。", "placeholders": { "seatcount": { "content": "$1", @@ -8217,7 +8233,7 @@ } }, "teamsStarterPlanInvLimitReachedNoManageBilling": { - "message": "团队入门版计划最多拥有 $SEATCOUNT$ 位成员。要升级您的计划并邀请更多成员,请联系您的组织所有者。", + "message": "团队入门版方案最多拥有 $SEATCOUNT$ 位成员。要升级您的方案并邀请更多成员,请联系您的组织所有者。", "placeholders": { "seatcount": { "content": "$1", @@ -8226,7 +8242,7 @@ } }, "freeOrgMaxCollectionReachedManageBilling": { - "message": "免费组织最多拥有 $COLLECTIONCOUNT$ 个集合。要添加更多集合,请升级到付费计划。", + "message": "免费组织最多拥有 $COLLECTIONCOUNT$ 个集合。要添加更多集合,请升级到付费方案。", "placeholders": { "COLLECTIONCOUNT": { "content": "$1", @@ -8934,7 +8950,7 @@ "message": "邀请用户" }, "secretsManagerForPlan": { - "message": "适用于 $PLAN$ 的机密管理器", + "message": "$PLAN$ 机密管理器", "placeholders": { "plan": { "content": "$1", @@ -8985,13 +9001,13 @@ "message": "订阅机密管理器" }, "addSecretsManagerUpgradeDesc": { - "message": "将机密管理器添加到升级后的计划中,以保留对使用之前的计划创建的所有机密的访问权限。" + "message": "将机密管理器添加到升级后的方案中,以保留对使用之前的方案创建的所有机密的访问权限。" }, "additionalServiceAccounts": { "message": "附加服务账户" }, "includedServiceAccounts": { - "message": "您的计划包含 $COUNT$ 个服务账户。", + "message": "您的方案包含 $COUNT$ 个服务账户。", "placeholders": { "count": { "content": "$1", @@ -9035,10 +9051,10 @@ "message": "更新了集合管理设置" }, "passwordManagerPlanPrice": { - "message": "密码管理器计划价格" + "message": "密码管理器方案价格" }, "secretsManagerPlanPrice": { - "message": "机密管理器计划价格" + "message": "机密管理器方案价格" }, "passwordManager": { "message": "密码管理器" @@ -9600,7 +9616,7 @@ "message": "附加机器账户" }, "includedMachineAccounts": { - "message": "您的计划包含 $COUNT$ 个机器账户。", + "message": "您的方案包含 $COUNT$ 个机器账户。", "placeholders": { "count": { "content": "$1", @@ -9845,7 +9861,7 @@ "message": "索引" }, "selectAPlan": { - "message": "选择一个计划" + "message": "选择一个方案" }, "thirtyFivePercentDiscount": { "message": "35% 折扣" @@ -9993,10 +10009,10 @@ "message": "保护您的家庭或企业" }, "upgradeOrganizationCloseSecurityGaps": { - "message": "通过监控报告弥补安全漏洞" + "message": "通过监测报告弥补安全漏洞" }, "upgradeOrganizationCloseSecurityGapsDesc": { - "message": "升级到付费计划以加强监控,从而提前发现安全漏洞。" + "message": "升级到付费方案以加强监测,从而提前发现安全漏洞。" }, "approveAllRequests": { "message": "批准所有请求" @@ -10045,7 +10061,7 @@ "message": "通过审计成员访问权限来识别安全风险" }, "onlyAvailableForEnterpriseOrganization": { - "message": "通过升级为企业版计划,快速查看整个组织的成员访问权限。" + "message": "通过升级为企业版方案,快速查看整个组织的成员访问权限。" }, "date": { "message": "日期" @@ -10329,7 +10345,7 @@ "message": "SSO 身份验证" }, "familiesPlanInvLimitReachedManageBilling": { - "message": "家庭组织最多拥有 $SEATCOUNT$ 位成员。要邀请更多成员,请升级到付费计划。", + "message": "家庭组织最多拥有 $SEATCOUNT$ 位成员。要邀请更多成员,请升级到付费方案。", "placeholders": { "seatcount": { "content": "$1", @@ -10347,7 +10363,7 @@ } }, "upgradePlans": { - "message": "升级您的计划以邀请成员并体验强大的安全功能。" + "message": "升级您的方案以邀请成员并体验强大的安全功能。" }, "upgradeDiscount": { "message": "节省 $AMOUNT$%", @@ -10359,7 +10375,7 @@ } }, "enterprisePlanUpgradeMessage": { - "message": "适用于大型组织的高级功能" + "message": "为大型组织提供高级功能" }, "teamsPlanUpgradeMessage": { "message": "为成长中的团队提供弹性保护" @@ -10458,13 +10474,13 @@ "message": "当前" }, "secretsManagerSubscriptionInfo": { - "message": "您的机密管理器订阅将基于选择的计划升级" + "message": "您的机密管理器订阅将基于选择的方案升级" }, "bitwardenPasswordManager": { "message": "Bitwarden 密码管理器" }, "secretsManagerComplimentaryPasswordManager": { - "message": "您的一年免费密码管理器订阅将升级到所选计划。在免费期结束前,我们不会向您收取费用。" + "message": "您的一年免费密码管理器订阅将升级到所选方案。在免费期结束前,我们不会向您收取费用。" }, "fileSavedToDevice": { "message": "文件已保存到设备。可以在设备下载中进行管理。" @@ -10539,7 +10555,7 @@ "description": "This represents the beginning of a sentence. The full sentence will be 'Manage subscription from the Provider Portal', but 'Provider Portal' will be a link and thus cannot be included in the translation file." }, "toHostBitwardenOnYourOwnServer": { - "message": "要在您自己的服务器上托管 Bitwarden,您需要上传许可证文件。要支持自托管组织的免费家庭计划和高级计费功能,您需要在自托管组织中设置自动同步。" + "message": "要在您自己的服务器上托管 Bitwarden,您需要上传许可证文件。要支持自托管组织的免费家庭版方案和高级计费功能,您需要在自托管组织中设置自动同步。" }, "selfHostingTitleProper": { "message": "自托管" @@ -10632,7 +10648,7 @@ "message": "禁用免费 Bitwarden 家庭赞助" }, "freeFamiliesSponsorshipPolicyDesc": { - "message": "不允许成员通过此组织兑换家庭计划。" + "message": "不允许成员通过此组织兑换家庭版方案。" }, "verifyBankAccountWithStatementDescriptorWarning": { "message": "使用银行账户付款仅对美国用户开放。您将被要求验证您的银行账户。我们将在 1-2 个工作日内进行一笔小额转账,请在组织的计费页面输入该转账的对账单描述符代码以验证银行账户。验证银行账户失败将会错过支付,您的订阅将暂停。" @@ -10722,7 +10738,7 @@ } }, "updatedRevokeSponsorshipConfirmationForSentSponsorship": { - "message": "如果您移除 $EMAIL$,将无法兑换此家庭计划赞助。确定要继续吗?", + "message": "如果您移除 $EMAIL$,将无法兑换此家庭版方案赞助。确定要继续吗?", "placeholders": { "email": { "content": "$1", @@ -10731,7 +10747,7 @@ } }, "updatedRevokeSponsorshipConfirmationForAcceptedSponsorship": { - "message": "如果您移除 $EMAIL$,此家庭计划赞助将终止,并且将于 $DATE$ 向已保存的付款方式收取 $40 + 相关税费。在 $DATE$ 之前您将无法兑换新的赞助。确定要继续吗?", + "message": "如果您移除 $EMAIL$,此家庭版方案赞助将终止,并且将于 $DATE$ 向已保存的付款方式收取 $40 + 相关税费。在 $DATE$ 之前您将无法兑换新的赞助。确定要继续吗?", "placeholders": { "email": { "content": "$1", @@ -10991,7 +11007,7 @@ "message": "不允许成员使用 PIN 码解锁他们的账户。" }, "upgradeForFullEventsMessage": { - "message": "不会为您的组织存储事件日志。升级到团队计划或企业计划以获取组织事件日志的完整访问权限。" + "message": "不会为您的组织存储事件日志。升级到团队版或企业版方案以获取组织事件日志的完整访问权限。" }, "upgradeEventLogTitleMessage": { "message": "升级以查看您组织中的事件日志。" @@ -11003,7 +11019,19 @@ "message": "查看事件" }, "cannotCreateCollection": { - "message": "免费组织最多拥有 2 个集合。要添加更多集合,请升级到付费计划。" + "message": "免费组织最多拥有 2 个集合。要添加更多集合,请升级到付费方案。" + }, + "searchArchive": { + "message": "搜索归档" + }, + "archive": { + "message": "归档" + }, + "noItemsInArchive": { + "message": "归档中没有项目" + }, + "archivedItemsDescription": { + "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, "businessUnit": { "message": "业务单元" @@ -11143,7 +11171,7 @@ "message": "请点击「使用 PayPal 付款」按钮以添加您的付款方式。" }, "revokeActiveSponsorshipConfirmation": { - "message": "如果您移除 $EMAIL$,此家庭计划的赞助将结束。在被赞助组织的续费日期 $DATE$ 之后,您的组织中将释放一个可用席位,可供成员或赞助使用。", + "message": "如果您移除 $EMAIL$,此家庭版方案赞助将终止。在被赞助组织的续费日期 $DATE$ 之后,您的组织中将释放一个可用席位,可供成员或赞助使用。", "placeholders": { "email": { "content": "$1", @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": " 立即验证。" + }, + "additionalStorageGB": { + "message": "附加存储 GB" + }, + "additionalServiceAccountsV2": { + "message": "附加机器账户" + }, + "secretsManagerSeats": { + "message": "机密管理器席位" + }, + "additionalStorage": { + "message": "附加存储" + }, + "expandPurchaseDetails": { + "message": "展开购买详细信息" + }, + "collapsePurchaseDetails": { + "message": "折叠购买详细信息" + }, + "familiesMembership": { + "message": "家庭成员" + }, + "planDescPremium": { + "message": "全面的在线安全防护" + }, + "planDescFamiliesV2": { + "message": "为您的家庭提供高级安全防护" + }, + "planDescFreeV2": { + "message": "与 $COUNT$ 位其他用户共享", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "为任何组织提供高级功能" + }, + "planNameCustom": { + "message": "自定义方案" + }, + "planDescCustom": { + "message": "Bitwarden 适用于各种规模的企业,为密码和敏感信息提供安全保障。若您属于大型企业,请联系销售人员获取报价。" + }, + "builtInAuthenticator": { + "message": "内置身份验证器" + }, + "breachMonitoring": { + "message": "数据泄露监测" + }, + "andMoreFeatures": { + "message": "以及更多!" + }, + "secureFileStorage": { + "message": "安全文件存储" + }, + "familiesUnlimitedSharing": { + "message": "不限数量的共享 - 自主掌控可见范围" + }, + "familiesUnlimitedCollections": { + "message": "不限数量的家庭集合" + }, + "familiesSharedStorage": { + "message": "为重要的家庭信息提供共享存储" + }, + "limitedUsersV2": { + "message": "最多 $COUNT$ 位成员", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "最多 $COUNT$ 个集合", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "永远免费" + }, + "twoSecretsIncluded": { + "message": "2 个机密" + }, + "projectsIncludedV2": { + "message": "$COUNT$ 个工程", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "安全项目共享" + }, + "scimSupport": { + "message": "SCIM 支持" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ 个机器账户", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "企业安全策略" + }, + "selfHostOption": { + "message": "自托管选项" + }, + "complimentaryFamiliesPlan": { + "message": "为所有用户提供免费家庭版方案" + }, + "strengthenCybersecurity": { + "message": "加强网络安全" + }, + "boostProductivity": { + "message": "提高工作效率" + }, + "seamlessIntegration": { + "message": "无缝集成" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 91c4dfdbcda..4f9a78c2c02 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -5557,6 +5557,13 @@ "personalOwnershipSubmitError": { "message": "由於某個企業原則,您被限制為儲存項目至您的個人密碼庫。將擁有權變更為組織,並從可用的集合中選擇。" }, + "desktopAutotypePolicy": { + "message": "Desktop Autotype Default Setting" + }, + "desktopAutotypePolicyDesc": { + "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "description": "This policy will enable Desktop Autotype by default for members on Unlock." + }, "disableSend": { "message": "停用 Send" }, @@ -6701,7 +6708,7 @@ "message": "Key Connector" }, "memberDecryptionKeyConnectorDescStart": { - "message": "將 SSO 登入連接到您的自我裝載解密金鑰伺服器。使用此選項,成員無需使用其主密碼來解密密碼庫資料。需要", + "message": "將 SSO 登入連結至您的自架解密金鑰伺服器。使用此選項,成員無需使用其主密碼來解密密碼庫資料。需要", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescLink": { @@ -6709,7 +6716,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescEnd": { - "message": "以用於設定 Key Connector 解密。聯絡 Bitwarden 支援以獲取設定協助。", + "message": "以用於設定 Key Connector 解密。聯絡 Bitwarden 支援以取得設定協助。", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "keyConnectorPolicyRestriction": { @@ -6721,6 +6728,15 @@ "disabledSso": { "message": "已停用 SSO" }, + "emailMustLoginWithSso": { + "message": "$EMAIL$ must login with Single Sign-on", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, "enabledKeyConnector": { "message": "已啟用 Key Connector" }, @@ -6770,7 +6786,7 @@ "message": "Generate billing token" }, "copyPasteBillingSync": { - "message": "請將本權杖複製後,貼至您自我裝載組織的「計費同步」設定當中。" + "message": "請將本權杖複製後,貼至您自架組織的「計費同步」設定當中。" }, "billingSyncCanAccess": { "message": "您的計費同步權杖可以讀取及編輯此組織的訂閱設定。" @@ -6788,25 +6804,25 @@ "message": "輪換權杖" }, "rotateBillingSyncTokenWarning": { - "message": "如果繼續,您需要重新在自我裝載伺服器上設定計費同步。" + "message": "如果繼續,您需要重新在自架伺服器上設定計費同步。" }, "rotateBillingSyncTokenTitle": { "message": "輪換「計費同步權杖」會導致之前的權杖失效。" }, "selfHostedServer": { - "message": "自建" + "message": "自架" }, "customEnvironment": { "message": "自訂環境" }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "指定您自架的 Bitwarden 伺服器的網域 URL。例如:https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "適用於進階設定。您可以單獨指定各個服務的網域 URL。" }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "您必須新增伺服器網域 URL 或至少一個自訂環境。" }, "apiUrl": { "message": "API 伺服器網址" @@ -6827,10 +6843,10 @@ "message": "環境網址已儲存" }, "selfHostingTitle": { - "message": "自我裝載" + "message": "自架" }, "selfHostingEnterpriseOrganizationSectionCopy": { - "message": "若要在您自己的伺服器上設定組織,您需要上傳授權檔案。若要使您的自我裝載組織支援「免費家庭」方案及進階計費功能,您需要設定計費同步。" + "message": "若要在您自己的伺服器上設定組織,您需要上傳授權檔案。若要使您的自架組織支援「免費家庭」方案及進階計費功能,您需要設定計費同步。" }, "billingSyncApiKeyRotated": { "message": "權杖已輪換" @@ -7129,7 +7145,7 @@ } }, "awaitingSyncSingular": { - "message": "權杖已在 $DAYS$ 天前輪換。請在您的自我裝載組織設定中,更新計費同步權杖。", + "message": "權杖已在 $DAYS$ 天前輪換。請在您的自架組織設定中,更新計費同步權杖。", "placeholders": { "days": { "content": "$1", @@ -7138,7 +7154,7 @@ } }, "awaitingSyncPlural": { - "message": "權杖已在 $DAYS$ 天前輪換。請在您的自我裝載組織設定中,更新計費同步權杖。", + "message": "權杖已在 $DAYS$ 天前輪換。請在您的自架組織設定中,更新計費同步權杖。", "placeholders": { "days": { "content": "$1", @@ -7151,7 +7167,7 @@ "description": "Used as a prefix to indicate the last time a sync occurred. Example \"Last sync 1968-11-16 00:00:00\"" }, "sponsorshipsSynced": { - "message": "已同步自我裝載贊助。" + "message": "已同步自架贊助。" }, "billingManagedByProvider": { "message": "由 $PROVIDER$ 管理", @@ -9074,7 +9090,7 @@ "message": "伺服器網址" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "自架伺服器 URL", "description": "Label for field requesting a self-hosted integration service URL" }, "alreadyHaveAccount": { @@ -10542,7 +10558,7 @@ "message": "To host Bitwarden on your own server, you will need to upload your license file. To support Free Families plans and advanced billing capabilities for your self-hosted organization, you will need to set up automatic sync in your self-hosted organization." }, "selfHostingTitleProper": { - "message": "Self-Hosting" + "message": "自架" }, "claim-domain-single-org-warning": { "message": "Claiming a domain will turn on the single organization policy." @@ -11005,6 +11021,18 @@ "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + }, "businessUnit": { "message": "Business Unit" }, @@ -11337,5 +11365,137 @@ }, "verifyNow": { "message": "Verify now." + }, + "additionalStorageGB": { + "message": "Additional storage GB" + }, + "additionalServiceAccountsV2": { + "message": "Additional machine accounts" + }, + "secretsManagerSeats": { + "message": "Secrets Manager seats" + }, + "additionalStorage": { + "message": "Additional Storage" + }, + "expandPurchaseDetails": { + "message": "Expand purchase details" + }, + "collapsePurchaseDetails": { + "message": "Collapse purchase details" + }, + "familiesMembership": { + "message": "Families membership" + }, + "planDescPremium": { + "message": "Complete online security" + }, + "planDescFamiliesV2": { + "message": "Premium security for your family" + }, + "planDescFreeV2": { + "message": "Share with $COUNT$ other user", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "planDescEnterpriseV2": { + "message": "Advanced capabilities for any organization" + }, + "planNameCustom": { + "message": "Custom plan" + }, + "planDescCustom": { + "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + }, + "builtInAuthenticator": { + "message": "Built-in authenticator" + }, + "breachMonitoring": { + "message": "Breach monitoring" + }, + "andMoreFeatures": { + "message": "And more!" + }, + "secureFileStorage": { + "message": "Secure file storage" + }, + "familiesUnlimitedSharing": { + "message": "Unlimited sharing - choose who sees what" + }, + "familiesUnlimitedCollections": { + "message": "Unlimited family collections" + }, + "familiesSharedStorage": { + "message": "Shared storage for important family info" + }, + "limitedUsersV2": { + "message": "Up to $COUNT$ members", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "limitedCollectionsV2": { + "message": "Up to $COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "alwaysFree": { + "message": "Always free" + }, + "twoSecretsIncluded": { + "message": "2 secrets" + }, + "projectsIncludedV2": { + "message": "$COUNT$ project(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "1" + } + } + }, + "secureItemSharing": { + "message": "Secure item sharing" + }, + "scimSupport": { + "message": "SCIM support" + }, + "includedMachineAccountsV2": { + "message": "$COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "enterpriseSecurityPolicies": { + "message": "Enterprise security policies" + }, + "selfHostOption": { + "message": "Self-host option" + }, + "complimentaryFamiliesPlan": { + "message": "Complimentary families plan for all users" + }, + "strengthenCybersecurity": { + "message": "Strengthen cybersecurity" + }, + "boostProductivity": { + "message": "Boost productivity" + }, + "seamlessIntegration": { + "message": "Seamless integration" } } From 466bf18d51127b7b7b01a257b2e1941adb70c8c7 Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:53:08 -0500 Subject: [PATCH 12/55] [PM-25614] Add Encrichment Logic for Risk Insights Data Service (#16577) * Add encryption logic. Minor updates to critical apps service * Fix possibly null type --- .../risk-insights/models/report-models.ts | 2 +- .../services/critical-apps.service.spec.ts | 12 +- .../services/critical-apps.service.ts | 81 +++++++----- .../services/risk-insights-data.service.ts | 122 ++++++++++++++++-- .../services/risk-insights-report.service.ts | 55 ++++++-- .../access-intelligence.module.ts | 13 +- .../critical-applications.component.ts | 68 +++++----- .../risk-insights.component.ts | 5 +- 8 files changed, 265 insertions(+), 93 deletions(-) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts index 3fe40be7e1f..acbec1592a0 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts @@ -117,7 +117,7 @@ export type OrganizationReportApplication = { }; /** - * All applications report detail. Application is the cipher + * Report details for an application * uri. Has the at risk, password, and member information */ export type ApplicationHealthReportDetail = { diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.spec.ts index 067d3f887ea..72d7e88fcab 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.spec.ts @@ -70,7 +70,7 @@ describe("CriticalAppsService", () => { const orgKey$ = new BehaviorSubject(OrgRecords); keyService.orgKeys$.mockReturnValue(orgKey$); - service.setOrganizationId(SomeOrganization, SomeUser); + service.loadOrganizationContext(SomeOrganization, SomeUser); // act await service.setCriticalApps(SomeOrganization, criticalApps); @@ -112,7 +112,7 @@ describe("CriticalAppsService", () => { const orgKey$ = new BehaviorSubject(OrgRecords); keyService.orgKeys$.mockReturnValue(orgKey$); - service.setOrganizationId(SomeOrganization, SomeUser); + service.loadOrganizationContext(SomeOrganization, SomeUser); // act await service.setCriticalApps(SomeOrganization, selectedUrls); @@ -136,7 +136,7 @@ describe("CriticalAppsService", () => { const orgKey$ = new BehaviorSubject(OrgRecords); keyService.orgKeys$.mockReturnValue(orgKey$); - service.setOrganizationId(SomeOrganization, SomeUser); + service.loadOrganizationContext(SomeOrganization, SomeUser); expect(keyService.orgKeys$).toHaveBeenCalledWith(SomeUser); expect(encryptService.decryptString).toHaveBeenCalledTimes(2); @@ -154,7 +154,7 @@ describe("CriticalAppsService", () => { const orgKey$ = new BehaviorSubject(OrgRecords); keyService.orgKeys$.mockReturnValue(orgKey$); - service.setOrganizationId(SomeOrganization, SomeUser); + service.loadOrganizationContext(SomeOrganization, SomeUser); service.setAppsInListForOrg(response); service.getAppsListForOrg(orgId as OrganizationId).subscribe((res) => { expect(res).toHaveLength(2); @@ -173,7 +173,7 @@ describe("CriticalAppsService", () => { const orgKey$ = new BehaviorSubject(OrgRecords); keyService.orgKeys$.mockReturnValue(orgKey$); - service.setOrganizationId(SomeOrganization, SomeUser); + service.loadOrganizationContext(SomeOrganization, SomeUser); service.setAppsInListForOrg(initialList); @@ -204,7 +204,7 @@ describe("CriticalAppsService", () => { const orgKey$ = new BehaviorSubject(OrgRecords); keyService.orgKeys$.mockReturnValue(orgKey$); - service.setOrganizationId(SomeOrganization, SomeUser); + service.loadOrganizationContext(SomeOrganization, SomeUser); service.setAppsInListForOrg(initialList); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts index be17bb2c0a5..82001387bbd 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts @@ -7,9 +7,7 @@ import { map, Observable, of, - Subject, switchMap, - takeUntil, zip, } from "rxjs"; @@ -30,17 +28,16 @@ import { CriticalAppsApiService } from "./critical-apps-api.service"; * Encrypts and saves data for a given organization */ export class CriticalAppsService { - private orgId = new BehaviorSubject(null); + // -------------------------- Context state -------------------------- + // The organization ID of the organization the user is currently viewing + private organizationId = new BehaviorSubject(null); private orgKey$ = new Observable(); - private criticalAppsList = new BehaviorSubject([]); - private teardown = new Subject(); - private fetchOrg$ = this.orgId - .pipe( - switchMap((orgId) => this.retrieveCriticalApps(orgId)), - takeUntil(this.teardown), - ) - .subscribe((apps) => this.criticalAppsList.next(apps)); + // -------------------------- Data ------------------------------------ + private criticalAppsListSubject$ = new BehaviorSubject< + PasswordHealthReportApplicationsResponse[] + >([]); + criticalAppsList$ = this.criticalAppsListSubject$.asObservable(); constructor( private keyService: KeyService, @@ -48,25 +45,52 @@ export class CriticalAppsService { private criticalAppsApiService: CriticalAppsApiService, ) {} + // Set context for the service for a specific organization + loadOrganizationContext(orgId: OrganizationId, userId: UserId) { + // Fetch the organization key for the user + this.orgKey$ = this.keyService.orgKeys$(userId).pipe( + filter((OrgKeys) => !!OrgKeys), + map((organizationKeysById) => organizationKeysById[orgId as OrganizationId]), + ); + + // Store organization id for service context + this.organizationId.next(orgId); + + // Setup the critical apps fetching for the organization + if (orgId) { + this.retrieveCriticalApps(orgId).subscribe({ + next: (result) => { + this.criticalAppsListSubject$.next(result); + }, + error: (error: unknown) => { + throw error; + }, + }); + } + } + // Get a list of critical apps for a given organization getAppsListForOrg(orgId: OrganizationId): Observable { - if (orgId != this.orgId.value) { - throw new Error("Organization ID mismatch"); + // [FIXME] Get organization id from context for all functions in this file + if (orgId != this.organizationId.value) { + throw new Error( + `Organization ID mismatch: expected ${this.organizationId.value}, got ${orgId}`, + ); } - return this.criticalAppsList + return this.criticalAppsListSubject$ .asObservable() .pipe(map((apps) => apps.filter((app) => app.organizationId === orgId))); } // Reset the critical apps list setAppsInListForOrg(apps: PasswordHealthReportApplicationsResponse[]) { - this.criticalAppsList.next(apps); + this.criticalAppsListSubject$.next(apps); } // Save the selected critical apps for a given organization async setCriticalApps(orgId: OrganizationId, selectedUrls: string[]) { - if (orgId != this.orgId.value) { + if (orgId != this.organizationId.value) { throw new Error("Organization ID mismatch"); } @@ -79,7 +103,7 @@ export class CriticalAppsService { // only save records that are not already in the database const newEntries = await this.filterNewEntries(orgId as OrganizationId, selectedUrls); const criticalAppsRequests = await this.encryptNewEntries( - this.orgId.value as OrganizationId, + this.organizationId.value as OrganizationId, orgKey, newEntries, ); @@ -89,7 +113,7 @@ export class CriticalAppsService { ); // add the new entries to the criticalAppsList - const updatedList = [...this.criticalAppsList.value]; + const updatedList = [...this.criticalAppsListSubject$.value]; for (const responseItem of dbResponse) { const decryptedUrl = await this.encryptService.decryptString( new EncString(responseItem.uri), @@ -103,26 +127,17 @@ export class CriticalAppsService { } as PasswordHealthReportApplicationsResponse); } } - this.criticalAppsList.next(updatedList); - } - - // Get the critical apps for a given organization - setOrganizationId(orgId: OrganizationId, userId: UserId) { - this.orgKey$ = this.keyService.orgKeys$(userId).pipe( - filter((OrgKeys) => !!OrgKeys), - map((organizationKeysById) => organizationKeysById[orgId as OrganizationId]), - ); - this.orgId.next(orgId); + this.criticalAppsListSubject$.next(updatedList); } // Drop a critical app for a given organization // Only one app may be dropped at a time async dropCriticalApp(orgId: OrganizationId, selectedUrl: string) { - if (orgId != this.orgId.value) { + if (orgId != this.organizationId.value) { throw new Error("Organization ID mismatch"); } - const app = this.criticalAppsList.value.find( + const app = this.criticalAppsListSubject$.value.find( (f) => f.organizationId === orgId && f.uri === selectedUrl, ); @@ -135,7 +150,9 @@ export class CriticalAppsService { passwordHealthReportApplicationIds: [app.id], }); - this.criticalAppsList.next(this.criticalAppsList.value.filter((f) => f.uri !== selectedUrl)); + this.criticalAppsListSubject$.next( + this.criticalAppsListSubject$.value.filter((f) => f.uri !== selectedUrl), + ); } private retrieveCriticalApps( @@ -170,7 +187,7 @@ export class CriticalAppsService { } private async filterNewEntries(orgId: OrganizationId, selectedUrls: string[]): Promise { - return await firstValueFrom(this.criticalAppsList).then((criticalApps) => { + return await firstValueFrom(this.criticalAppsListSubject$).then((criticalApps) => { const criticalAppsUri = criticalApps .filter((f) => f.organizationId === orgId) .map((f) => f.uri); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts index f3736c517e7..f58c13a9cfb 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts @@ -1,7 +1,13 @@ -import { BehaviorSubject } from "rxjs"; -import { finalize } from "rxjs/operators"; +import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; +import { finalize, switchMap, withLatestFrom } from "rxjs/operators"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { AppAtRiskMembersDialogParams, @@ -9,14 +15,35 @@ import { AtRiskMemberDetail, DrawerType, ApplicationHealthReportDetail, + ApplicationHealthReportDetailEnriched, } from "../models/report-models"; +import { CriticalAppsService } from "./critical-apps.service"; import { RiskInsightsReportService } from "./risk-insights-report.service"; export class RiskInsightsDataService { - private applicationsSubject = new BehaviorSubject(null); + // -------------------------- Context state -------------------------- + // Current user viewing risk insights + private userIdSubject = new BehaviorSubject(null); + userId$ = this.userIdSubject.asObservable(); + // Organization the user is currently viewing + private organizationDetailsSubject = new BehaviorSubject<{ + organizationId: OrganizationId; + organizationName: string; + } | null>(null); + organizationDetails$ = this.organizationDetailsSubject.asObservable(); + + // -------------------------- Data ------------------------------------ + private applicationsSubject = new BehaviorSubject(null); applications$ = this.applicationsSubject.asObservable(); + private dataLastUpdatedSubject = new BehaviorSubject(null); + dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable(); + + criticalApps$ = this.criticalAppsService.criticalAppsList$; + + // --------------------------- UI State ------------------------------------ + private isLoadingSubject = new BehaviorSubject(false); isLoading$ = this.isLoadingSubject.asObservable(); @@ -26,9 +53,6 @@ export class RiskInsightsDataService { private errorSubject = new BehaviorSubject(null); error$ = this.errorSubject.asObservable(); - private dataLastUpdatedSubject = new BehaviorSubject(null); - dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable(); - openDrawer = false; drawerInvokerId: string = ""; activeDrawerType: DrawerType = DrawerType.None; @@ -36,7 +60,51 @@ export class RiskInsightsDataService { appAtRiskMembers: AppAtRiskMembersDialogParams | null = null; atRiskAppDetails: AtRiskApplicationDetail[] | null = null; - constructor(private reportService: RiskInsightsReportService) {} + constructor( + private accountService: AccountService, + private criticalAppsService: CriticalAppsService, + private organizationService: OrganizationService, + private reportService: RiskInsightsReportService, + ) {} + + // [FIXME] PM-25612 - Call Initialization in RiskInsightsComponent instead of child components + async initializeForOrganization(organizationId: OrganizationId) { + // Fetch current user + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + if (userId) { + this.userIdSubject.next(userId); + } + + // [FIXME] getOrganizationById is now deprecated - update when we can + // Fetch organization details + const org = await firstValueFrom( + this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)), + ); + if (org) { + this.organizationDetailsSubject.next({ + organizationId: organizationId, + organizationName: org.name, + }); + } + + // Load critical applications for organization + await this.criticalAppsService.loadOrganizationContext(organizationId, userId); + + // TODO: PM-25613 + // // Load existing report + + // this.fetchLastReport(organizationId, userId); + + // // Setup new report generation + // this._runApplicationsReport().subscribe({ + // next: (result) => { + // this.isRunningReportSubject.next(false); + // }, + // error: () => { + // this.errorSubject.next("Failed to save report"); + // }, + // }); + } /** * Fetches the applications report and updates the applicationsSubject. @@ -72,6 +140,44 @@ export class RiskInsightsDataService { this.fetchApplicationsReport(organizationId, true); } + // ------------------------------- Enrichment methods ------------------------------- + /** + * Takes the basic application health report details and enriches them to include + * critical app status and associated ciphers. + * + * @param applications The list of application health report details to enrich + * @returns The enriched application health report details with critical app status and ciphers + */ + enrichReportData$( + applications: ApplicationHealthReportDetail[], + ): Observable { + return of(applications).pipe( + withLatestFrom(this.organizationDetails$, this.criticalApps$), + switchMap(async ([apps, orgDetails, criticalApps]) => { + if (!orgDetails) { + return []; + } + + // Get ciphers for application + const cipherMap = await this.reportService.getApplicationCipherMap( + apps, + orgDetails.organizationId, + ); + + // Find critical apps + const criticalApplicationNames = new Set(criticalApps.map((ca) => ca.uri)); + + // Return enriched application data + return apps.map((app) => ({ + ...app, + ciphers: cipherMap.get(app.applicationName) || [], + isMarkedAsCritical: criticalApplicationNames.has(app.applicationName), + })) as ApplicationHealthReportDetailEnriched[]; + }), + ); + } + + // ------------------------------- Drawer management methods ------------------------------- isActiveDrawerType = (drawerType: DrawerType): boolean => { return this.activeDrawerType === drawerType; }; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts index e6843385833..7341beb3fe2 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts @@ -59,15 +59,6 @@ import { RiskInsightsApiService } from "./risk-insights-api.service"; import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service"; export class RiskInsightsReportService { - constructor( - private passwordStrengthService: PasswordStrengthServiceAbstraction, - private auditService: AuditService, - private cipherService: CipherService, - private memberCipherDetailsApiService: MemberCipherDetailsApiService, - private riskInsightsApiService: RiskInsightsApiService, - private riskInsightsEncryptionService: RiskInsightsEncryptionService, - ) {} - private riskInsightsReportSubject = new BehaviorSubject([]); riskInsightsReport$ = this.riskInsightsReportSubject.asObservable(); @@ -84,6 +75,27 @@ export class RiskInsightsReportService { }); riskInsightsSummary$ = this.riskInsightsSummarySubject.asObservable(); + // [FIXME] CipherData + // Cipher data + // private _ciphersSubject = new BehaviorSubject(null); + // _ciphers$ = this._ciphersSubject.asObservable(); + + constructor( + private passwordStrengthService: PasswordStrengthServiceAbstraction, + private auditService: AuditService, + private cipherService: CipherService, + private memberCipherDetailsApiService: MemberCipherDetailsApiService, + private riskInsightsApiService: RiskInsightsApiService, + private riskInsightsEncryptionService: RiskInsightsEncryptionService, + ) {} + + // [FIXME] CipherData + // async loadCiphersForOrganization(organizationId: OrganizationId): Promise { + // await this.cipherService.getAllFromApiForOrganization(organizationId).then((ciphers) => { + // this._ciphersSubject.next(ciphers); + // }); + // } + /** * Report data from raw cipher health data. * Can be used in the Raw Data diagnostic tab (just exclude the members in the view) @@ -559,6 +571,31 @@ export class RiskInsightsReportService { return applicationMap; } + /** + * + * @param applications The list of application health report details to map ciphers to + * @param organizationId + * @returns + */ + async getApplicationCipherMap( + applications: ApplicationHealthReportDetail[], + organizationId: OrganizationId, + ): Promise> { + // [FIXME] CipherData + // This call is made multiple times. We can optimize this + // by loading the ciphers once via a load method to avoid multiple API calls + // for the same organization + const allCiphers = await this.cipherService.getAllFromApiForOrganization(organizationId); + const cipherMap = new Map(); + + applications.forEach((app) => { + const filteredCiphers = allCiphers.filter((c) => app.cipherIds.includes(c.id)); + cipherMap.set(app.applicationName, filteredCiphers); + }); + return cipherMap; + } + + // --------------------------- Aggregation methods --------------------------- /** * Loop through the flattened cipher to uri data. If the item exists it's values need to be updated with the new item. * If the item is new, create and add the object with the flattened details diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts index 0fe1737bde3..c39f06a57a9 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts @@ -11,6 +11,8 @@ import { import { RiskInsightsEncryptionService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/risk-insights-encryption.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction"; @@ -36,10 +38,15 @@ import { RiskInsightsComponent } from "./risk-insights.component"; MemberCipherDetailsApiService, ], }, - { + safeProvider({ provide: RiskInsightsDataService, - deps: [RiskInsightsReportService], - }, + deps: [ + AccountServiceAbstraction, + CriticalAppsService, + OrganizationService, + RiskInsightsReportService, + ], + }), { provide: RiskInsightsEncryptionService, useClass: RiskInsightsEncryptionService, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts index c8bc3e81680..481ed39a004 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts @@ -66,40 +66,42 @@ export class CriticalApplicationsComponent implements OnInit { "organizationId", ) as OrganizationId; const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - this.criticalAppsService.setOrganizationId(this.organizationId as OrganizationId, userId); - // this.criticalAppsService.setOrganizationId(this.organizationId as OrganizationId); - combineLatest([ - this.dataService.applications$, - this.criticalAppsService.getAppsListForOrg(this.organizationId as OrganizationId), - ]) - .pipe( - takeUntilDestroyed(this.destroyRef), - map(([applications, criticalApps]) => { - const criticalUrls = criticalApps.map((ca) => ca.uri); - const data = applications?.map((app) => ({ - ...app, - isMarkedAsCritical: criticalUrls.includes(app.applicationName), - })) as LEGACY_ApplicationHealthReportDetailWithCriticalFlag[]; - return data?.filter((app) => app.isMarkedAsCritical); - }), - switchMap(async (data) => { - if (data) { - const dataWithCiphers = await this.reportService.identifyCiphers( - data, - this.organizationId, - ); - return dataWithCiphers; + this.criticalAppsService.loadOrganizationContext(this.organizationId as OrganizationId, userId); + + if (this.organizationId) { + combineLatest([ + this.dataService.applications$, + this.criticalAppsService.getAppsListForOrg(this.organizationId as OrganizationId), + ]) + .pipe( + takeUntilDestroyed(this.destroyRef), + map(([applications, criticalApps]) => { + const criticalUrls = criticalApps.map((ca) => ca.uri); + const data = applications?.map((app) => ({ + ...app, + isMarkedAsCritical: criticalUrls.includes(app.applicationName), + })) as LEGACY_ApplicationHealthReportDetailWithCriticalFlag[]; + return data?.filter((app) => app.isMarkedAsCritical); + }), + switchMap(async (data) => { + if (data) { + const dataWithCiphers = await this.reportService.identifyCiphers( + data, + this.organizationId, + ); + return dataWithCiphers; + } + return null; + }), + ) + .subscribe((applications) => { + if (applications) { + this.dataSource.data = applications; + this.applicationSummary = this.reportService.generateApplicationsSummary(applications); + this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0; } - return null; - }), - ) - .subscribe((applications) => { - if (applications) { - this.dataSource.data = applications; - this.applicationSummary = this.reportService.generateApplicationsSummary(applications); - this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0; - } - }); + }); + } } goToAllAppsTab = async () => { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index b7e440880e3..8279ae612e9 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -127,7 +127,10 @@ export class RiskInsightsComponent implements OnInit { this.appsCount = applications.length; } - this.criticalAppsService.setOrganizationId(this.organizationId as OrganizationId, userId); + this.criticalAppsService.loadOrganizationContext( + this.organizationId as OrganizationId, + userId, + ); this.criticalApps$ = this.criticalAppsService.getAppsListForOrg( this.organizationId as OrganizationId, ); From e5c5bf63cab1bbd48bf9f2219cb88d97517c8b79 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 17:02:39 +0200 Subject: [PATCH 13/55] [deps] Platform: Update @types/chrome to v0.1.0 (#15697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [deps] Platform: Update @types/chrome to v0.1.0 * Fix typing * Fix other build errors * Fix strict compile * Update pkg and fix remaining type errors --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García --- .../overlay-notifications.background.ts | 2 +- .../auto-submit-login.background.spec.ts | 20 ++++---- .../auto-submit-login.background.ts | 29 ++++++----- .../overlay-notifications.background.spec.ts | 22 ++++----- .../overlay-notifications.background.ts | 27 +++++++---- .../background/overlay.background.spec.ts | 4 +- .../autofill/background/overlay.background.ts | 2 +- .../autofill/background/tabs.background.ts | 2 +- .../background/web-request.background.ts | 26 +++++----- .../fido2/background/fido2.background.ts | 5 +- .../content/fido2-content-script.spec.ts | 6 ++- .../src/autofill/spec/testing-utils.ts | 8 ++-- .../browser/src/background/idle.background.ts | 2 +- .../services/phishing-detection.service.ts | 4 +- .../services/phishing-detection.types.ts | 2 +- ...r-api.register-content-scripts-polyfill.ts | 48 ++++++++++++------- .../src/platform/browser/browser-api.spec.ts | 8 ++-- .../src/platform/browser/browser-api.ts | 18 ++++--- .../chrome-storage-api.service.spec.ts | 10 ++-- .../browser-script-injector.service.spec.ts | 5 +- .../browser-script-injector.service.ts | 6 +-- .../browser-platform-utils.service.spec.ts | 4 +- .../browser-system-notification.service.ts | 3 +- apps/browser/test.setup.ts | 1 + package-lock.json | 8 ++-- package.json | 2 +- 26 files changed, 157 insertions(+), 117 deletions(-) diff --git a/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts b/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts index 71452ec975a..a70ffe25310 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts @@ -19,7 +19,7 @@ export type LoginSecurityTaskInfo = { export type WebsiteOriginsWithFields = Map>; -export type ActiveFormSubmissionRequests = Set; +export type ActiveFormSubmissionRequests = Set; export type ModifyLoginCipherFormData = { uri: string; diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts index 373354b4c54..82a907a9e43 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts @@ -110,11 +110,11 @@ describe("AutoSubmitLoginBackground", () => { }); describe("when the AutomaticAppLogIn policy is valid and active", () => { - let webRequestDetails: chrome.webRequest.WebRequestBodyDetails; + let webRequestDetails: chrome.webRequest.WebRequestDetails; describe("starting the auto-submit login workflow", () => { beforeEach(async () => { - webRequestDetails = mock({ + webRequestDetails = mock({ initiator: validIpdUrl1, url: validAutoSubmitUrl, type: "main_frame", @@ -196,7 +196,7 @@ describe("AutoSubmitLoginBackground", () => { describe("cancelling an active auto-submit login workflow", () => { beforeEach(async () => { - webRequestDetails = mock({ + webRequestDetails = mock({ initiator: validIpdUrl1, url: validAutoSubmitUrl, type: "main_frame", @@ -280,7 +280,7 @@ describe("AutoSubmitLoginBackground", () => { }); describe("requests that occur within a sub-frame", () => { - const webRequestDetails = mock({ + const webRequestDetails = mock({ url: validAutoSubmitUrl, frameId: 1, }); @@ -324,7 +324,7 @@ describe("AutoSubmitLoginBackground", () => { it("updates the most recent idp host when a tab is activated", async () => { jest.spyOn(BrowserApi, "getTab").mockResolvedValue(newTab); - triggerTabOnActivatedEvent(mock({ tabId: newTabId })); + triggerTabOnActivatedEvent(mock({ tabId: newTabId })); await flushPromises(); expect(autoSubmitLoginBackground["mostRecentIdpHost"]).toStrictEqual({ @@ -336,7 +336,7 @@ describe("AutoSubmitLoginBackground", () => { it("updates the most recent id host when a tab is updated", () => { triggerTabOnUpdatedEvent( newTabId, - mock({ url: validIpdUrl1 }), + mock({ url: validIpdUrl1 }), newTab, ); @@ -389,7 +389,7 @@ describe("AutoSubmitLoginBackground", () => { tabId: newTabId, }; - triggerTabOnRemovedEvent(newTabId, mock()); + triggerTabOnRemovedEvent(newTabId, mock()); expect(autoSubmitLoginBackground["currentAutoSubmitHostData"]).toStrictEqual({}); }); @@ -403,14 +403,14 @@ describe("AutoSubmitLoginBackground", () => { tabId: tabId, }; triggerWebRequestOnBeforeRedirectEvent( - mock({ + mock({ url: validIpdUrl1, redirectUrl: validIpdUrl2, frameId: 0, }), ); triggerWebRequestOnBeforeRedirectEvent( - mock({ + mock({ url: validIpdUrl2, redirectUrl: validAutoSubmitUrl, frameId: 0, @@ -418,7 +418,7 @@ describe("AutoSubmitLoginBackground", () => { ); triggerWebRequestOnBeforeRequestEvent( - mock({ + mock({ tabId: tabId, url: `https://${validAutoSubmitHost}`, initiator: null, diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.ts b/apps/browser/src/autofill/background/auto-submit-login.background.ts index dfdfa0f4d67..f593fab2516 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.ts @@ -161,7 +161,9 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * * @param details - The details of the request. */ - private handleOnBeforeRequest = (details: chrome.webRequest.WebRequestBodyDetails) => { + private handleOnBeforeRequest = ( + details: chrome.webRequest.OnBeforeRequestDetails, + ): undefined => { const requestInitiator = this.getRequestInitiator(details); const isValidInitiator = this.isValidInitiator(requestInitiator); @@ -191,7 +193,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * @param isValidInitiator - A flag indicating if the initiator of the request is valid. */ private postRequestEncounteredAfterSubmission = ( - details: chrome.webRequest.WebRequestBodyDetails, + details: chrome.webRequest.OnBeforeRequestDetails, isValidInitiator: boolean, ) => { return details.method === "POST" && this.validAutoSubmitHosts.size > 0 && isValidInitiator; @@ -205,7 +207,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * @param isValidInitiator - A flag indicating if the initiator of the request is valid. */ private requestRedirectsToInvalidHost = ( - details: chrome.webRequest.WebRequestBodyDetails, + details: chrome.webRequest.OnBeforeRequestDetails, isValidInitiator: boolean, ) => { return ( @@ -221,7 +223,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * * @param details - The details of the request. */ - private setupAutoSubmitFlow = (details: chrome.webRequest.WebRequestBodyDetails) => { + private setupAutoSubmitFlow = (details: chrome.webRequest.OnBeforeRequestDetails) => { if (this.isRequestInMainFrame(details)) { this.currentAutoSubmitHostData = { url: details.url, @@ -288,7 +290,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * @param details - The details of the request. */ private handleWebRequestOnBeforeRedirect = ( - details: chrome.webRequest.WebRedirectionResponseDetails, + details: chrome.webRequest.OnBeforeRedirectDetails, ) => { if (this.isRequestInMainFrame(details) && this.urlContainsAutoSubmitHash(details.redirectUrl)) { this.validAutoSubmitHosts.add(this.getUrlHost(details.redirectUrl)); @@ -354,7 +356,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr */ private disableAutoSubmitFlow = async ( requestInitiator: string, - details: chrome.webRequest.WebRequestBodyDetails, + details: chrome.webRequest.OnBeforeRequestDetails, ) => { if (this.isValidAutoSubmitHost(requestInitiator)) { this.removeUrlFromAutoSubmitHosts(requestInitiator); @@ -390,7 +392,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * @param initiator - The initiator of the request. */ private shouldRouteTriggerAutoSubmit = ( - details: chrome.webRequest.ResourceRequest, + details: chrome.webRequest.OnBeforeRequestDetails, initiator: string, ) => { if (this.isRequestInMainFrame(details)) { @@ -449,7 +451,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * * @param details - The details of the request. */ - private getRequestInitiator = (details: chrome.webRequest.ResourceRequest) => { + private getRequestInitiator = (details: chrome.webRequest.OnBeforeRequestDetails) => { if (!this.isSafariBrowser) { return details.initiator || (details as browser.webRequest._OnBeforeRequestDetails).originUrl; } @@ -470,7 +472,12 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * * @param details - The details of the request. */ - private isRequestInMainFrame = (details: chrome.webRequest.ResourceRequest) => { + private isRequestInMainFrame = ( + details: SetPartial< + chrome.webRequest.WebRequestDetails, + "documentId" | "documentLifecycle" | "frameType" + >, + ) => { if (this.isSafariBrowser) { return details.frameId === 0; } @@ -545,7 +552,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * * @param activeInfo - The active tab information. */ - private handleSafariTabOnActivated = async (activeInfo: chrome.tabs.TabActiveInfo) => { + private handleSafariTabOnActivated = async (activeInfo: chrome.tabs.OnActivatedInfo) => { if (activeInfo.tabId < 0) { return; } @@ -562,7 +569,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * @param tabId - The tab ID associated with the URL. * @param changeInfo - The change information of the tab. */ - private handleSafariTabOnUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { + private handleSafariTabOnUpdated = (tabId: number, changeInfo: chrome.tabs.OnUpdatedInfo) => { if (changeInfo) { this.setMostRecentIdpHost(changeInfo.url, tabId); } diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts index cf317de4fd2..c596a1ba774 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts @@ -385,7 +385,7 @@ describe("OverlayNotificationsBackground", () => { it("ignores requests that are not part of an active form submission", async () => { triggerWebRequestOnCompletedEvent( - mock({ + mock({ url: sender.url, tabId: sender.tab.id, requestId: "123345", @@ -409,7 +409,7 @@ describe("OverlayNotificationsBackground", () => { await flushPromises(); triggerWebRequestOnCompletedEvent( - mock({ + mock({ url: sender.url, tabId: sender.tab.id, requestId, @@ -438,7 +438,7 @@ describe("OverlayNotificationsBackground", () => { await flushPromises(); triggerWebRequestOnCompletedEvent( - mock({ + mock({ url: sender.url, tabId: sender.tab.id, statusCode: 404, @@ -492,7 +492,7 @@ describe("OverlayNotificationsBackground", () => { ); }); triggerWebRequestOnCompletedEvent( - mock({ + mock({ url: sender.url, tabId: sender.tab.id, requestId, @@ -541,7 +541,7 @@ describe("OverlayNotificationsBackground", () => { }); triggerWebRequestOnCompletedEvent( - mock({ + mock({ url: sender.url, tabId: sender.tab.id, requestId, @@ -643,7 +643,7 @@ describe("OverlayNotificationsBackground", () => { }); it("clears all associated data with a removed tab", () => { - triggerTabOnRemovedEvent(sender.tab.id, mock()); + triggerTabOnRemovedEvent(sender.tab.id, mock()); expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0); }); @@ -652,7 +652,7 @@ describe("OverlayNotificationsBackground", () => { it("skips clearing the website origins if the changeInfo does not contain a `loading` status", () => { triggerTabOnUpdatedEvent( sender.tab.id, - mock({ status: "complete" }), + mock({ status: "complete" }), mock({ status: "complete" }), ); @@ -662,7 +662,7 @@ describe("OverlayNotificationsBackground", () => { it("skips clearing the website origins if the changeInfo does not contain a url", () => { triggerTabOnUpdatedEvent( sender.tab.id, - mock({ status: "loading", url: "" }), + mock({ status: "loading", url: "" }), mock({ status: "loading" }), ); @@ -672,7 +672,7 @@ describe("OverlayNotificationsBackground", () => { it("skips clearing the website origins if the tab does not contain known website origins", () => { triggerTabOnUpdatedEvent( 199, - mock({ status: "loading", url: "https://example.com" }), + mock({ status: "loading", url: "https://example.com" }), mock({ status: "loading", id: 199 }), ); @@ -682,7 +682,7 @@ describe("OverlayNotificationsBackground", () => { it("skips clearing the website origins if the changeInfo's url is present as part of the know website origin match patterns", () => { triggerTabOnUpdatedEvent( sender.tab.id, - mock({ + mock({ status: "loading", url: "https://subdomain.example.com", }), @@ -695,7 +695,7 @@ describe("OverlayNotificationsBackground", () => { it("clears all associated data with a tab that is entering a `loading` state", () => { triggerTabOnUpdatedEvent( sender.tab.id, - mock({ status: "loading" }), + mock({ status: "loading" }), mock({ status: "loading" }), ); diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts index e7126a57e9f..4657dfb6d1f 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -228,7 +228,9 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * * @param details - The details of the web request */ - private handleOnBeforeRequestEvent = (details: chrome.webRequest.WebRequestDetails) => { + private handleOnBeforeRequestEvent = ( + details: chrome.webRequest.OnBeforeRequestDetails, + ): undefined => { if (this.isPostSubmissionFormRedirection(details)) { this.setupNotificationInitTrigger( details.tabId, @@ -275,7 +277,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * * @param details - The details of the web request */ - private isPostSubmissionFormRedirection = (details: chrome.webRequest.WebRequestDetails) => { + private isPostSubmissionFormRedirection = (details: chrome.webRequest.OnBeforeRequestDetails) => { return ( details.method?.toUpperCase() === "GET" && this.activeFormSubmissionRequests.has(details.requestId) && @@ -289,7 +291,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * * @param details - The details of the web request */ - private isValidFormSubmissionRequest = (details: chrome.webRequest.WebRequestDetails) => { + private isValidFormSubmissionRequest = (details: chrome.webRequest.OnBeforeRequestDetails) => { return ( !this.requestHostIsInvalid(details) && this.formSubmissionRequestMethods.has(details.method?.toUpperCase()) @@ -325,7 +327,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * * @param details - The details of the web response */ - private handleOnCompletedRequestEvent = async (details: chrome.webRequest.WebResponseDetails) => { + private handleOnCompletedRequestEvent = async (details: chrome.webRequest.OnCompletedDetails) => { if ( this.requestHostIsInvalid(details) || !this.activeFormSubmissionRequests.has(details.requestId) @@ -382,8 +384,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * @param modifyLoginData - The modified login form data */ private delayNotificationInitUntilTabIsComplete = async ( - tabId: chrome.webRequest.ResourceRequest["tabId"], - requestId: chrome.webRequest.ResourceRequest["requestId"], + tabId: chrome.webRequest.WebRequestDetails["tabId"], + requestId: chrome.webRequest.WebRequestDetails["requestId"], modifyLoginData: ModifyLoginCipherFormData, ) => { const handleWebNavigationOnCompleted = async () => { @@ -403,7 +405,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * @param tab - The tab details */ private processNotifications = async ( - requestId: chrome.webRequest.ResourceRequest["requestId"], + requestId: chrome.webRequest.WebRequestDetails["requestId"], modifyLoginData: ModifyLoginCipherFormData, tab: chrome.tabs.Tab, config: { skippable: NotificationType[] } = { skippable: [] }, @@ -477,7 +479,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * @param tab - The tab details */ private clearCompletedWebRequest = ( - requestId: chrome.webRequest.ResourceRequest["requestId"], + requestId: chrome.webRequest.WebRequestDetails["requestId"], tabId: chrome.tabs.Tab["id"], ) => { this.activeFormSubmissionRequests.delete(requestId); @@ -492,7 +494,12 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * * @param details - The details of the web request */ - private requestHostIsInvalid = (details: chrome.webRequest.ResourceRequest) => { + private requestHostIsInvalid = ( + details: SetPartial< + chrome.webRequest.WebRequestDetails, + "documentId" | "documentLifecycle" | "frameType" + >, + ) => { return !details.url?.startsWith("http") || details.tabId < 0; }; @@ -553,7 +560,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * @param tabId - The id of the tab that was updated * @param changeInfo - The change info of the tab */ - private handleTabUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { + private handleTabUpdated = (tabId: number, changeInfo: chrome.tabs.OnUpdatedInfo) => { if (changeInfo.status !== "loading" || !changeInfo.url) { return; } diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 696454b4248..47a5e8fec4c 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -3371,7 +3371,7 @@ describe("OverlayBackground", () => { }); await flushPromises(); triggerWebRequestOnCompletedEvent( - mock({ + mock({ statusCode: 401, }), ); @@ -3391,7 +3391,7 @@ describe("OverlayBackground", () => { }); await flushPromises(); triggerWebRequestOnCompletedEvent( - mock({ + mock({ statusCode: 200, }), ); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 5da00cfaff2..35585d58863 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1238,7 +1238,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param details - The web request details */ private handlePasskeyAuthenticationOnCompleted = ( - details: chrome.webRequest.WebResponseCacheDetails, + details: chrome.webRequest.OnCompletedDetails, ) => { chrome.webRequest.onCompleted.removeListener(this.handlePasskeyAuthenticationOnCompleted); diff --git a/apps/browser/src/autofill/background/tabs.background.ts b/apps/browser/src/autofill/background/tabs.background.ts index cd2c1595d69..b76997c0ae9 100644 --- a/apps/browser/src/autofill/background/tabs.background.ts +++ b/apps/browser/src/autofill/background/tabs.background.ts @@ -81,7 +81,7 @@ export default class TabsBackground { */ private handleTabOnUpdated = async ( tabId: number, - changeInfo: chrome.tabs.TabChangeInfo, + changeInfo: chrome.tabs.OnUpdatedInfo, tab: chrome.tabs.Tab, ) => { if (this.focusedWindowId > 0 && tab.windowId !== this.focusedWindowId) { diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts index 22e10a3dd0a..5c02f2df34d 100644 --- a/apps/browser/src/autofill/background/web-request.background.ts +++ b/apps/browser/src/autofill/background/web-request.background.ts @@ -26,7 +26,10 @@ export default class WebRequestBackground { startListening() { this.webRequest.onAuthRequired.addListener( - async (details, callback) => { + (async ( + details: chrome.webRequest.OnAuthRequiredDetails, + callback: (response: chrome.webRequest.BlockingResponse) => void, + ) => { if (!details.url || this.pendingAuthRequests.has(details.requestId)) { if (callback) { callback(null); @@ -42,7 +45,7 @@ export default class WebRequestBackground { } else { await this.resolveAuthCredentials(details.url, callback, callback); } - }, + }) as any, { urls: ["http://*/*", "https://*/*"] }, [this.isFirefox ? "blocking" : "asyncBlocking"], ); @@ -50,16 +53,17 @@ export default class WebRequestBackground { this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), { urls: ["http://*/*"], }); - this.webRequest.onErrorOccurred.addListener( - (details: any) => this.completeAuthRequest(details), - { - urls: ["http://*/*"], - }, - ); + this.webRequest.onErrorOccurred.addListener((details) => this.completeAuthRequest(details), { + urls: ["http://*/*"], + }); } - // eslint-disable-next-line - private async resolveAuthCredentials(domain: string, success: Function, error: Function) { + private async resolveAuthCredentials( + domain: string, + success: (response: chrome.webRequest.BlockingResponse) => void, + // eslint-disable-next-line + error: Function, + ) { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(getOptionalUserId), ); @@ -97,7 +101,7 @@ export default class WebRequestBackground { } } - private completeAuthRequest(details: chrome.webRequest.WebResponseCacheDetails) { + private completeAuthRequest(details: chrome.webRequest.WebRequestDetails) { this.pendingAuthRequests.delete(details.requestId); } } diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.ts b/apps/browser/src/autofill/fido2/background/fido2.background.ts index 788c98ca85b..22ee4a1822d 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.ts @@ -218,7 +218,10 @@ export class Fido2Background implements Fido2BackgroundInterface { tabId: tab.id, injectDetails: { frame: "all_frames", ...this.sharedInjectionDetails }, mv2Details: { file: await this.getFido2PageScriptAppendFileName() }, - mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" }, + mv3Details: { + file: Fido2ContentScript.PageScript, + world: chrome.scripting.ExecutionWorld.MAIN, + }, }); void this.scriptInjectorService.inject({ diff --git a/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts index af7344beb66..f50e541f677 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts @@ -75,7 +75,7 @@ describe("Fido2 Content Script", () => { data: mock(), }); const mockResult = { credentialId: "mock" } as CreateCredentialResult; - jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue(mockResult); + (jest.spyOn(chrome.runtime, "sendMessage") as jest.Mock).mockResolvedValue(mockResult); // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -167,7 +167,9 @@ describe("Fido2 Content Script", () => { data: mock(), }); const abortController = new AbortController(); - jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue({ error: errorMessage }); + (jest.spyOn(chrome.runtime, "sendMessage") as jest.Mock).mockResolvedValue({ + error: errorMessage, + }); // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-require-imports diff --git a/apps/browser/src/autofill/spec/testing-utils.ts b/apps/browser/src/autofill/spec/testing-utils.ts index 1a3f3a52234..0082f022fb6 100644 --- a/apps/browser/src/autofill/spec/testing-utils.ts +++ b/apps/browser/src/autofill/spec/testing-utils.ts @@ -80,7 +80,7 @@ export function triggerWindowOnFocusedChangedEvent(windowId: number) { ); } -export function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) { +export function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.OnActivatedInfo) { (chrome.tabs.onActivated.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; @@ -98,7 +98,7 @@ export function triggerTabOnReplacedEvent(addedTabId: number, removedTabId: numb export function triggerTabOnUpdatedEvent( tabId: number, - changeInfo: chrome.tabs.TabChangeInfo, + changeInfo: chrome.tabs.OnUpdatedInfo, tab: chrome.tabs.Tab, ) { (chrome.tabs.onUpdated.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { @@ -107,7 +107,7 @@ export function triggerTabOnUpdatedEvent( }); } -export function triggerTabOnRemovedEvent(tabId: number, removeInfo: chrome.tabs.TabRemoveInfo) { +export function triggerTabOnRemovedEvent(tabId: number, removeInfo: chrome.tabs.OnRemovedInfo) { (chrome.tabs.onRemoved.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(tabId, removeInfo); @@ -165,7 +165,7 @@ export function triggerWebRequestOnBeforeRedirectEvent( }); } -export function triggerWebRequestOnCompletedEvent(details: chrome.webRequest.WebResponseDetails) { +export function triggerWebRequestOnCompletedEvent(details: chrome.webRequest.OnCompletedDetails) { (chrome.webRequest.onCompleted.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 81a869917a6..2de4b48a9c0 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -46,7 +46,7 @@ export default class IdleBackground { if (this.idle.onStateChanged) { this.idle.onStateChanged.addListener( - async (newState: chrome.idle.IdleState | browser.idle.IdleState) => { + async (newState: `${chrome.idle.IdleState}` | browser.idle.IdleState) => { if (newState === "locked") { // Need to check if any of the current users have their timeout set to `onLocked` const allUsers = await firstValueFrom(this.accountService.accounts$); diff --git a/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.ts b/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.ts index dd7cf083a01..1497ac96dba 100644 --- a/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.ts +++ b/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.ts @@ -230,7 +230,7 @@ export class PhishingDetectionService { */ private static async _processNavigation( tabId: number, - changeInfo: chrome.tabs.TabChangeInfo, + changeInfo: chrome.tabs.OnUpdatedInfo, tab: chrome.tabs.Tab, ): Promise { if (changeInfo.status !== "complete" || !tab.url) { @@ -253,7 +253,7 @@ export class PhishingDetectionService { private static _handleNavigationEvent( tabId: number, - changeInfo: chrome.tabs.TabChangeInfo, + changeInfo: chrome.tabs.OnUpdatedInfo, tab: chrome.tabs.Tab, ): boolean { this._navigationEventsSubject.next({ tabId, changeInfo, tab }); diff --git a/apps/browser/src/dirt/phishing-detection/services/phishing-detection.types.ts b/apps/browser/src/dirt/phishing-detection/services/phishing-detection.types.ts index 86fe61909c4..21793616241 100644 --- a/apps/browser/src/dirt/phishing-detection/services/phishing-detection.types.ts +++ b/apps/browser/src/dirt/phishing-detection/services/phishing-detection.types.ts @@ -30,6 +30,6 @@ export type CaughtPhishingDomain = { export type PhishingDetectionNavigationEvent = { tabId: number; - changeInfo: chrome.tabs.TabChangeInfo; + changeInfo: chrome.tabs.OnUpdatedInfo; tab: chrome.tabs.Tab; }; diff --git a/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts index 0b0dd21824b..a8e7b7eeb37 100644 --- a/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts +++ b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts @@ -141,8 +141,24 @@ function buildRegisterContentScriptsPolyfill() { return [possibleArray]; } - function arrayOrUndefined(value?: number) { - return value === undefined ? undefined : [value]; + function createTarget( + tabId: number, + frameId: number | undefined, + allFrames: boolean, + ): chrome.scripting.InjectionTarget { + if (frameId === undefined) { + return { + tabId, + frameIds: undefined, + allFrames: allFrames, + }; + } else { + return { + tabId, + frameIds: [frameId], + allFrames: undefined, + }; + } } async function insertCSS( @@ -170,15 +186,17 @@ function buildRegisterContentScriptsPolyfill() { } if (gotScripting) { - return chrome.scripting.insertCSS({ - target: { - tabId, - frameIds: arrayOrUndefined(frameId), - allFrames: frameId === undefined ? allFrames : undefined, - }, - files: "file" in content ? [content.file] : undefined, - css: "code" in content ? content.code : undefined, - }); + if ("file" in content) { + return chrome.scripting.insertCSS({ + target: createTarget(tabId, frameId, allFrames), + files: [content.file], + }); + } else { + return chrome.scripting.insertCSS({ + target: createTarget(tabId, frameId, allFrames), + css: content.code, + }); + } } return chromeProxy.tabs.insertCSS(tabId, { @@ -226,11 +244,7 @@ function buildRegisterContentScriptsPolyfill() { if (gotScripting) { assertNoCode(normalizedFiles); const injection = chrome.scripting.executeScript({ - target: { - tabId, - frameIds: arrayOrUndefined(frameId), - allFrames: frameId === undefined ? allFrames : undefined, - }, + target: createTarget(tabId, frameId, allFrames), files: normalizedFiles.map(({ file }: { file: string }) => file), }); @@ -397,7 +411,7 @@ function buildRegisterContentScriptsPolyfill() { }; const tabListener = async ( tabId: number, - { status }: chrome.tabs.TabChangeInfo, + { status }: chrome.tabs.OnUpdatedInfo, { url }: chrome.tabs.Tab, ) => { if (status === "loading" && url) { diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index 49d3e8e1cec..f7561b2b50b 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -375,7 +375,7 @@ describe("BrowserApi", () => { describe("executeScriptInTab", () => { it("calls to the extension api to execute a script within the give tabId", async () => { const tabId = 1; - const injectDetails = mock(); + const injectDetails = mock(); jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2); (chrome.tabs.executeScript as jest.Mock).mockImplementation( (tabId, injectDetails, callback) => callback(executeScriptResult), @@ -393,7 +393,7 @@ describe("BrowserApi", () => { it("calls the manifest v3 scripting API if the extension manifest is for v3", async () => { const tabId = 1; - const injectDetails = mock({ + const injectDetails = mock({ file: "file.js", allFrames: true, runAt: "document_start", @@ -419,7 +419,7 @@ describe("BrowserApi", () => { it("injects the script into a specified frameId when the extension is built for manifest v3", async () => { const tabId = 1; const frameId = 2; - const injectDetails = mock({ + const injectDetails = mock({ file: "file.js", allFrames: true, runAt: "document_start", @@ -443,7 +443,7 @@ describe("BrowserApi", () => { it("injects the script into the MAIN world context when injecting a script for manifest v3", async () => { const tabId = 1; - const injectDetails = mock({ + const injectDetails = mock({ file: null, allFrames: true, runAt: "document_start", diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 339fd71b071..8a3dbafc5ce 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -685,29 +685,27 @@ export class BrowserApi { */ static executeScriptInTab( tabId: number, - details: chrome.tabs.InjectDetails, + details: chrome.extensionTypes.InjectDetails, scriptingApiDetails?: { world: chrome.scripting.ExecutionWorld; }, ): Promise { if (BrowserApi.isManifestVersion(3)) { - const target: chrome.scripting.InjectionTarget = { - tabId, - }; + let target: chrome.scripting.InjectionTarget; if (typeof details.frameId === "number") { - target.frameIds = [details.frameId]; - } - - if (!target.frameIds?.length && details.allFrames) { - target.allFrames = details.allFrames; + target = { tabId, frameIds: [details.frameId] }; + } else if (details.allFrames) { + target = { tabId, allFrames: true }; + } else { + target = { tabId }; } return chrome.scripting.executeScript({ target, files: details.file ? [details.file] : null, injectImmediately: details.runAt === "document_start", - world: scriptingApiDetails?.world || "ISOLATED", + world: scriptingApiDetails?.world || chrome.scripting.ExecutionWorld.ISOLATED, }); } diff --git a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts index ac8a01375fa..c1d0aa235fb 100644 --- a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts +++ b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts @@ -52,7 +52,7 @@ describe("ChromeStorageApiService", () => { }); afterEach(() => { - chrome.runtime.lastError = undefined; + (chrome.runtime.lastError as any) = undefined; }); it("uses `objToStore` to prepare a value for set", async () => { @@ -80,7 +80,7 @@ describe("ChromeStorageApiService", () => { it("translates chrome.runtime.lastError to promise rejection", async () => { setMock.mockImplementation((data, callback) => { - chrome.runtime.lastError = new Error("Test Error"); + (chrome.runtime.lastError as any) = new Error("Test Error"); callback(); }); @@ -101,7 +101,7 @@ describe("ChromeStorageApiService", () => { }); afterEach(() => { - chrome.runtime.lastError = undefined; + (chrome.runtime.lastError as any) = undefined; }); it("returns a stored value when it is serialized", async () => { @@ -132,9 +132,9 @@ describe("ChromeStorageApiService", () => { it("translates chrome.runtime.lastError to promise rejection", async () => { getMock.mockImplementation((key, callback) => { - chrome.runtime.lastError = new Error("Test Error"); + (chrome.runtime.lastError as any) = new Error("Test Error"); callback(); - chrome.runtime.lastError = undefined; + (chrome.runtime.lastError as any) = undefined; }); await expect(async () => await service.get("test")).rejects.toThrow("Test Error"); diff --git a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts index 21f6debc02f..e4abea1d719 100644 --- a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts +++ b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts @@ -41,7 +41,10 @@ describe("ScriptInjectorService", () => { const mv2SpecificFile = "content/autofill-init-mv2.js"; const mv2Details = { file: mv2SpecificFile }; const mv3SpecificFile = "content/autofill-init-mv3.js"; - const mv3Details: Mv3ScriptInjectionDetails = { file: mv3SpecificFile, world: "MAIN" }; + const mv3Details: Mv3ScriptInjectionDetails = { + file: mv3SpecificFile, + world: chrome.scripting.ExecutionWorld.MAIN, + }; const sharedInjectDetails: CommonScriptInjectionDetails = { runAt: "document_start", }; diff --git a/apps/browser/src/platform/services/browser-script-injector.service.ts b/apps/browser/src/platform/services/browser-script-injector.service.ts index a617f2215c0..575e9ccf70b 100644 --- a/apps/browser/src/platform/services/browser-script-injector.service.ts +++ b/apps/browser/src/platform/services/browser-script-injector.service.ts @@ -63,7 +63,7 @@ export class BrowserScriptInjectorService extends ScriptInjectorService { if (BrowserApi.isManifestVersion(3)) { try { await BrowserApi.executeScriptInTab(tabId, injectionDetails, { - world: mv3Details?.world ?? "ISOLATED", + world: mv3Details?.world ?? chrome.scripting.ExecutionWorld.ISOLATED, }); } catch (error) { // Swallow errors for host permissions, since this is believed to be a Manifest V3 Chrome bug @@ -112,9 +112,9 @@ export class BrowserScriptInjectorService extends ScriptInjectorService { private buildInjectionDetails( injectDetails: CommonScriptInjectionDetails, file: string, - ): chrome.tabs.InjectDetails { + ): chrome.extensionTypes.InjectDetails { const { frame, runAt } = injectDetails; - const injectionDetails: chrome.tabs.InjectDetails = { file }; + const injectionDetails: chrome.extensionTypes.InjectDetails = { file }; if (runAt) { injectionDetails.runAt = runAt; diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index 81e1008eea8..61e56f08e16 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -167,7 +167,7 @@ describe("Browser Utils Service", () => { it("returns false if special error is sent", async () => { chrome.runtime.sendMessage = jest.fn().mockImplementation((message, callback) => { - chrome.runtime.lastError = new Error( + (chrome.runtime.lastError as any) = new Error( "Could not establish connection. Receiving end does not exist.", ); callback(undefined); @@ -177,7 +177,7 @@ describe("Browser Utils Service", () => { expect(isViewOpen).toBe(false); - chrome.runtime.lastError = null; + (chrome.runtime.lastError as any) = null; }); }); diff --git a/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts b/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts index 0eb4739ea92..b835c711853 100644 --- a/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts +++ b/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts @@ -46,7 +46,7 @@ export class BrowserSystemNotificationService implements SystemNotificationsServ return new Promise((resolve) => { const deviceType: DeviceType = this.platformUtilsService.getDevice(); - const options: chrome.notifications.NotificationOptions = { + const options: chrome.notifications.NotificationCreateOptions = { iconUrl: chrome.runtime.getURL("images/icon128.png"), message: createInfo.body, type: "basic", @@ -70,6 +70,7 @@ export class BrowserSystemNotificationService implements SystemNotificationsServ } async clear(clearInfo: SystemNotificationClearInfo): Promise { + // eslint-disable-next-line @typescript-eslint/no-floating-promises chrome.notifications.clear(clearInfo.id); } diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index eb635e646e1..08b7287984c 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -79,6 +79,7 @@ const scripting = { executeScript: jest.fn(), registerContentScripts: jest.fn(), unregisterContentScripts: jest.fn(), + ExecutionWorld: { ISOLATED: "ISOLATED", MAIN: "MAIN" }, }; const windows = { diff --git a/package-lock.json b/package-lock.json index 2ea907ec598..1b126255e63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,7 +97,7 @@ "@storybook/test-runner": "0.22.0", "@storybook/theming": "8.6.12", "@storybook/web-components-webpack5": "8.6.12", - "@types/chrome": "0.0.306", + "@types/chrome": "0.1.12", "@types/firefox-webext-browser": "120.0.4", "@types/inquirer": "8.2.10", "@types/jest": "29.5.14", @@ -13377,9 +13377,9 @@ } }, "node_modules/@types/chrome": { - "version": "0.0.306", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.306.tgz", - "integrity": "sha512-95kgcqvTNcaZCXmx/kIKY6uo83IaRNT3cuPxYqlB2Iu+HzKDCP4t7TUe7KhJijTdibcvn+SzziIcfSLIlgRnhQ==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.12.tgz", + "integrity": "sha512-jEkxs9GPQHx7g49WjkA8QDNcqODbMGDuBbWQOtjiS/Wf9AiEcDmQMIAgJvC/Xi36WoCVNx584g0Dd9ThJQCAiw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 65ead3fffc5..e94d0e98522 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@storybook/test-runner": "0.22.0", "@storybook/theming": "8.6.12", "@storybook/web-components-webpack5": "8.6.12", - "@types/chrome": "0.0.306", + "@types/chrome": "0.1.12", "@types/firefox-webext-browser": "120.0.4", "@types/inquirer": "8.2.10", "@types/jest": "29.5.14", From 7baf25028884fcf67b1ff9eefa22f608c092bd17 Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Fri, 26 Sep 2025 11:31:31 -0400 Subject: [PATCH 14/55] [PM-26192] Integrate the new Autotype Default Policy (#16604) * Add current WIP autotype policy integration work * [PM-26192] Add working code that fully integrates the autotype default policy * [PM-26192] Add comments * Update apps/desktop/src/autofill/services/desktop-autotype.service.ts Co-authored-by: Jonathan Prusik --------- Co-authored-by: Jonathan Prusik --- .../src/app/services/services.module.ts | 1 + .../desktop-autotype-policy.service.ts | 2 +- .../services/desktop-autotype.service.ts | 34 +++++++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index ea16f36402c..9f2bb1acc90 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -468,6 +468,7 @@ const safeProviders: SafeProvider[] = [ GlobalStateProvider, PlatformUtilsServiceAbstraction, BillingAccountProfileStateService, + DesktopAutotypeDefaultSettingPolicy, ], }), safeProvider({ diff --git a/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts b/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts index 76ffc090600..887a30ef6f6 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts @@ -21,7 +21,7 @@ export class DesktopAutotypeDefaultSettingPolicy { ) {} /** - * Emits the autotype policy enabled status (true | false | null) when account is unlocked and WindowsDesktopAutotype is enabled. + * Emits the autotype policy enabled status when account is unlocked and WindowsDesktopAutotype is enabled. * - true: autotype policy exists and is enabled * - null: no autotype policy exists for the user's organization */ diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.ts index 60e87aa2aa5..b156ffd3597 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.ts @@ -17,7 +17,9 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { UserId } from "@bitwarden/user-core"; -export const AUTOTYPE_ENABLED = new KeyDefinition( +import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service"; + +export const AUTOTYPE_ENABLED = new KeyDefinition( AUTOTYPE_SETTINGS_DISK, "autotypeEnabled", { deserializer: (b) => b }, @@ -37,6 +39,7 @@ export class DesktopAutotypeService { private globalStateProvider: GlobalStateProvider, private platformUtilsService: PlatformUtilsService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private desktopAutotypePolicy: DesktopAutotypeDefaultSettingPolicy, ) { ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => { const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle); @@ -50,9 +53,32 @@ export class DesktopAutotypeService { } async init() { - this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$; - + // Currently Autotype is only supported for Windows if (this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop) { + // If `autotypeDefaultPolicy` is `true` for a user's organization, and the + // user has never changed their local autotype setting (`autotypeEnabledState`), + // we set their local setting to `true` (once the local user setting is changed + // by this policy or the user themselves, the default policy should + // never change the user setting again). + combineLatest([ + this.autotypeEnabledState.state$, + this.desktopAutotypePolicy.autotypeDefaultSetting$, + ]) + .pipe( + map(async ([autotypeEnabledState, autotypeDefaultPolicy]) => { + if (autotypeDefaultPolicy === true && autotypeEnabledState === null) { + await this.setAutotypeEnabledState(true); + } + }), + ) + .subscribe(); + + // autotypeEnabledUserSetting$ publicly represents the value the + // user has set for autotyeEnabled in their local settings. + this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$; + + // resolvedAutotypeEnabled$ represents the final determination if the Autotype + // feature should be on or off. this.resolvedAutotypeEnabled$ = combineLatest([ this.autotypeEnabledState.state$, this.configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype), @@ -76,6 +102,8 @@ export class DesktopAutotypeService { ), ); + // When the resolvedAutotypeEnabled$ value changes, this might require + // hotkey registration / deregistration in the main process. this.resolvedAutotypeEnabled$.subscribe((enabled) => { ipc.autofill.configureAutotype(enabled); }); From 8ba22f308005978a0a2328877cc0faf7861e4b20 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:59:38 -0400 Subject: [PATCH 15/55] [PM-25609] use password health service (#16482) * isValidCipher and findWeakPasswordDetails * auditPasswordLeaks$ * missing deps fix * refactor: remove unused dependencies from RiskInsightsReportService - Remove PasswordStrengthServiceAbstraction and AuditService from constructor - Update module dependency injection to only provide these services to PasswordHealthService - Remove unused imports and mock services from test file - Ensure proper separation of concerns where password health logic is centralized in PasswordHealthService --- .../reports/risk-insights/services/index.ts | 1 + .../risk-insights-report.service.spec.ts | 41 +-- .../services/risk-insights-report.service.ts | 243 +----------------- .../access-intelligence.module.ts | 15 +- 4 files changed, 51 insertions(+), 249 deletions(-) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts index f547df31f41..e3f75ea0daf 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts @@ -2,5 +2,6 @@ export * from "./member-cipher-details-api.service"; export * from "./password-health.service"; export * from "./critical-apps.service"; export * from "./critical-apps-api.service"; +export * from "./risk-insights-api.service"; export * from "./risk-insights-report.service"; export * from "./risk-insights-data.service"; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts index 45f9aeed1da..6c6fbb5b92c 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts @@ -1,10 +1,7 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { ZXCVBNResult } from "zxcvbn"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -16,6 +13,7 @@ import { MemberCipherDetailsResponse } from "../response/member-cipher-details.r import { mockCiphers } from "./ciphers.mock"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; import { mockMemberCipherDetails } from "./member-cipher-details-api.service.spec"; +import { PasswordHealthService } from "./password-health.service"; import { RiskInsightsApiService } from "./risk-insights-api.service"; import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service"; import { RiskInsightsReportService } from "./risk-insights-report.service"; @@ -24,10 +22,9 @@ describe("RiskInsightsReportService", () => { let service: RiskInsightsReportService; // Mock services - const pwdStrengthService = mock(); - const auditService = mock(); const cipherService = mock(); const memberCipherDetailsService = mock(); + const mockPasswordHealthService = mock(); const mockRiskInsightsApiService = mock(); const mockRiskInsightsEncryptionService = mock({ encryptRiskInsightsReport: jest.fn().mockResolvedValue("encryptedReportData"), @@ -40,26 +37,38 @@ describe("RiskInsightsReportService", () => { let mockMemberDetails: MemberCipherDetailsResponse[]; beforeEach(() => { - pwdStrengthService.getPasswordStrength.mockImplementation((password: string) => { - const score = password.length < 4 ? 1 : 4; - return { score } as ZXCVBNResult; - }); - - auditService.passwordLeaked.mockImplementation((password: string) => - Promise.resolve(password === "123" ? 100 : 0), - ); - cipherService.getAllFromApiForOrganization.mockResolvedValue(mockCiphers); memberCipherDetailsService.getMemberCipherDetails.mockResolvedValue(mockMemberCipherDetails); + // Mock PasswordHealthService methods + mockPasswordHealthService.isValidCipher.mockImplementation((cipher: any) => { + return ( + cipher.type === 1 && cipher.login?.password && !cipher.isDeleted && cipher.viewPassword + ); + }); + mockPasswordHealthService.findWeakPasswordDetails.mockImplementation((cipher: any) => { + if (cipher.login?.password === "123") { + return { score: 1, detailValue: { label: "veryWeak", badgeVariant: "danger" } }; + } + return null; + }); + mockPasswordHealthService.auditPasswordLeaks$.mockImplementation((ciphers: any[]) => { + const exposedDetails = ciphers + .filter((cipher) => cipher.login?.password === "123") + .map((cipher) => ({ + exposedXTimes: 100, + cipherId: cipher.id, + })); + return of(exposedDetails); + }); + service = new RiskInsightsReportService( - pwdStrengthService, - auditService, cipherService, memberCipherDetailsService, mockRiskInsightsApiService, mockRiskInsightsEncryptionService, + mockPasswordHealthService, ); // Reset mock ciphers before each test diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts index 7341beb3fe2..1839e89a1ae 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts @@ -3,27 +3,20 @@ import { BehaviorSubject, concatMap, - filter, first, firstValueFrom, forkJoin, from, map, - mergeMap, Observable, of, switchMap, - toArray, zip, } from "rxjs"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -37,10 +30,7 @@ import { import { LEGACY_CipherHealthReportDetail, LEGACY_CipherHealthReportUriDetail, - ExposedPasswordDetail, LEGACY_MemberDetailsFlat, - WeakPasswordDetail, - WeakPasswordScore, LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, } from "../models/password-health"; import { @@ -55,6 +45,7 @@ import { } from "../models/report-models"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; +import { PasswordHealthService } from "./password-health.service"; import { RiskInsightsApiService } from "./risk-insights-api.service"; import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service"; @@ -81,12 +72,11 @@ export class RiskInsightsReportService { // _ciphers$ = this._ciphersSubject.asObservable(); constructor( - private passwordStrengthService: PasswordStrengthServiceAbstraction, - private auditService: AuditService, private cipherService: CipherService, private memberCipherDetailsApiService: MemberCipherDetailsApiService, private riskInsightsApiService: RiskInsightsApiService, private riskInsightsEncryptionService: RiskInsightsEncryptionService, + private passwordHealthService: PasswordHealthService, ) {} // [FIXME] CipherData @@ -367,10 +357,12 @@ export class RiskInsightsReportService { ): Promise { const cipherHealthReports: LEGACY_CipherHealthReportDetail[] = []; const passwordUseMap = new Map(); - const exposedDetails = await this.findExposedPasswords(ciphers); + const exposedDetails = await firstValueFrom( + this.passwordHealthService.auditPasswordLeaks$(ciphers), + ); for (const cipher of ciphers) { - if (this.validateCipher(cipher)) { - const weakPassword = this.findWeakPassword(cipher); + if (this.passwordHealthService.isValidCipher(cipher)) { + const weakPassword = this.passwordHealthService.findWeakPasswordDetails(cipher); // Looping over all ciphers needs to happen first to determine reused passwords over all ciphers. // Store in the set and evaluate later if (passwordUseMap.has(cipher.login.password)) { @@ -448,104 +440,6 @@ export class RiskInsightsReportService { return appReports; } - private async findExposedPasswords(ciphers: CipherView[]): Promise { - const exposedDetails: ExposedPasswordDetail[] = []; - const promises: Promise[] = []; - - ciphers.forEach((ciph) => { - if (this.validateCipher(ciph)) { - const promise = this.auditService - .passwordLeaked(ciph.login.password) - .then((exposedCount) => { - if (exposedCount > 0) { - const detail = { - exposedXTimes: exposedCount, - cipherId: ciph.id, - } as ExposedPasswordDetail; - exposedDetails.push(detail); - } - }); - promises.push(promise); - } - }); - await Promise.all(promises); - - return exposedDetails; - } - - private findWeakPassword(cipher: CipherView): WeakPasswordDetail { - const hasUserName = this.isUserNameNotEmpty(cipher); - let userInput: string[] = []; - if (hasUserName) { - const atPosition = cipher.login.username.indexOf("@"); - if (atPosition > -1) { - userInput = userInput - .concat( - cipher.login.username - .substring(0, atPosition) - .trim() - .toLowerCase() - .split(/[^A-Za-z0-9]/), - ) - .filter((i) => i.length >= 3); - } else { - userInput = cipher.login.username - .trim() - .toLowerCase() - .split(/[^A-Za-z0-9]/) - .filter((i) => i.length >= 3); - } - } - const { score } = this.passwordStrengthService.getPasswordStrength( - cipher.login.password, - null, - userInput.length > 0 ? userInput : null, - ); - - if (score != null && score <= 2) { - const scoreValue = this.weakPasswordScore(score); - const weakPasswordDetail = { score: score, detailValue: scoreValue } as WeakPasswordDetail; - return weakPasswordDetail; - } - return null; - } - - private weakPasswordScore(score: number): WeakPasswordScore { - switch (score) { - case 4: - return { label: "strong", badgeVariant: "success" }; - case 3: - return { label: "good", badgeVariant: "primary" }; - case 2: - return { label: "weak", badgeVariant: "warning" }; - default: - return { label: "veryWeak", badgeVariant: "danger" }; - } - } - - private isUserNameNotEmpty(c: CipherView): boolean { - return !Utils.isNullOrWhitespace(c.login.username); - } - - /** - * Validates that the cipher is a login item, has a password - * is not deleted, and the user can view the password - * @param c the input cipher - */ - private validateCipher(c: CipherView): boolean { - const { type, login, isDeleted, viewPassword } = c; - if ( - type !== CipherType.Login || - login.password == null || - login.password === "" || - isDeleted || - !viewPassword - ) { - return false; - } - return true; - } - private _buildPasswordUseMap(ciphers: CipherView[]): Map { const passwordUseMap = new Map(); ciphers.forEach((cipher) => { @@ -696,11 +590,13 @@ export class RiskInsightsReportService { ciphers: CipherView[], memberDetails: MemberDetails[], ): Observable { - const validCiphers = ciphers.filter((cipher) => this.isValidCipher(cipher)); + const validCiphers = ciphers.filter((cipher) => + this.passwordHealthService.isValidCipher(cipher), + ); // Build password use map const passwordUseMap = this._buildPasswordUseMap(validCiphers); - return this.auditPasswordLeaks$(validCiphers).pipe( + return this.passwordHealthService.auditPasswordLeaks$(validCiphers).pipe( map((exposedDetails) => { return validCiphers.map((cipher) => { const exposedPassword = exposedDetails.find((x) => x.cipherId === cipher.id); @@ -710,7 +606,7 @@ export class RiskInsightsReportService { cipher: cipher, cipherMembers, healthData: { - weakPasswordDetail: this.findWeakPasswordDetails(cipher), + weakPasswordDetail: this.passwordHealthService.findWeakPasswordDetails(cipher), exposedPasswordDetail: exposedPassword, reusedPasswordCount: passwordUseMap.get(cipher.login.password) ?? 0, }, @@ -721,119 +617,4 @@ export class RiskInsightsReportService { }), ); } - - // TODO This is a temp implementation until the function is available in the password health service - /** - * Validates that the cipher is a login item, has a password - * is not deleted, and the user can view the password - * @param c the input cipher - */ - isValidCipher(c: CipherView): boolean { - const { type, login, isDeleted, viewPassword } = c; - if ( - type !== CipherType.Login || - login.password == null || - login.password === "" || - isDeleted || - !viewPassword - ) { - return false; - } - return true; - } - - // TODO This is a temp implementation until the function is available in the password health service - /** - * Extracts username parts from the cipher's username. - * This is used to help determine password strength. - * - * @param cipherUsername The username from the cipher. - * @returns An array of username parts. - */ - extractUsernameParts(cipherUsername: string) { - const atPosition = cipherUsername.indexOf("@"); - const userNameToProcess = - atPosition > -1 ? cipherUsername.substring(0, atPosition) : cipherUsername; - - return userNameToProcess - .trim() - .toLowerCase() - .split(/[^A-Za-z0-9]/) - .filter((i) => i.length >= 3); - } - - // TODO This is a temp implementation until the function is available in the password health service - /** - * Checks if the cipher has a weak password based on the password strength score. - * - * @param cipher - * @returns - */ - findWeakPasswordDetails(cipher: CipherView): WeakPasswordDetail | null { - // Validate the cipher - if (!this.isValidCipher(cipher)) { - return null; - } - - // Check the username - const userInput = this.isUserNameNotEmpty(cipher) - ? this.extractUsernameParts(cipher.login.username) - : null; - - const { score } = this.passwordStrengthService.getPasswordStrength( - cipher.login.password, - null, - userInput, - ); - - // If a score is not found or a score is less than 3, it's weak - if (score != null && score <= 2) { - return { score: score, detailValue: this.getPasswordScoreInfo(score) }; - } - return null; - } - - // TODO This is a temp implementation until the function is available in the password health service - /** - * Gets the password score information based on the score. - * - * @param score - * @returns An object containing the label and badge variant for the password score. - */ - getPasswordScoreInfo(score: number): WeakPasswordScore { - switch (score) { - case 4: - return { label: "strong", badgeVariant: "success" }; - case 3: - return { label: "good", badgeVariant: "primary" }; - case 2: - return { label: "weak", badgeVariant: "warning" }; - default: - return { label: "veryWeak", badgeVariant: "danger" }; - } - } - - // TODO This is a temp implementation until the function is available in the password health service - /** - * Finds exposed passwords in a list of ciphers. - * - * @param ciphers The list of ciphers to check. - * @returns An observable that emits an array of ExposedPasswordDetail. - */ - auditPasswordLeaks$(ciphers: CipherView[]): Observable { - return from(ciphers).pipe( - filter((cipher) => this.isValidCipher(cipher)), - mergeMap((cipher) => - this.auditService - .passwordLeaked(cipher.login.password) - .then((exposedCount) => ({ cipher, exposedCount })), - ), - filter(({ exposedCount }) => exposedCount > 0), - map(({ cipher, exposedCount }) => ({ - exposedXTimes: exposedCount, - cipherId: cipher.id, - })), - toArray(), - ); - } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts index c39f06a57a9..1d80f2154b1 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts @@ -5,6 +5,8 @@ import { CriticalAppsService } from "@bitwarden/bit-common/dirt/reports/risk-ins import { CriticalAppsApiService, MemberCipherDetailsApiService, + PasswordHealthService, + RiskInsightsApiService, RiskInsightsDataService, RiskInsightsReportService, } from "@bitwarden/bit-common/dirt/reports/risk-insights/services"; @@ -29,13 +31,22 @@ import { RiskInsightsComponent } from "./risk-insights.component"; provide: MemberCipherDetailsApiService, deps: [ApiService], }, + { + provide: PasswordHealthService, + deps: [PasswordStrengthServiceAbstraction, AuditService], + }, + { + provide: RiskInsightsApiService, + deps: [ApiService], + }, { provide: RiskInsightsReportService, deps: [ - PasswordStrengthServiceAbstraction, - AuditService, CipherService, MemberCipherDetailsApiService, + RiskInsightsApiService, + RiskInsightsEncryptionService, + PasswordHealthService, ], }, safeProvider({ From 979e370235b36822d1d801bd2c042415a71f4b5d Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:25:16 -0400 Subject: [PATCH 16/55] [PM-26031] Drawer Service State Refactoring (#16580) * refactor(risk-insights-data.service): unify drawer state management with BehaviorSubject - Replace individual drawer properties with unified drawerDetailsSubject - Add reactive Observable getters for drawer state checking - Update all drawer methods to use centralized state management * risk-insights.component: add special case drawer state sync in component - Add private _isDrawerOpen property for internal state tracking - Subscribe to drawerDetails$ changes with takeUntilDestroyed cleanup - Implement getter/setter for isDrawerOpen to sync component <-> service - Enable two-way binding while maintaining reactive patterns * risk-insights.component.html: replace drawer template with unified observable patterns - Replace dataService.openDrawer with isDrawerOpen special case getter - Wrap drawer in @if block with drawerDetails$ | async for single subscription - Update isActiveDrawerType() calls to reactive isActiveDrawerType$() | async - Replace direct property access with unified drawerDetails object - Use modern @if control flow syntax for better performance * all-applications.component.html: replace drawer state with reactive observable patterns - Replace dataService.drawerInvokerId with drawerDetails$ | async in card highlighting - Update app-table-row-scrollable input from isDrawerIsOpenForThisRecord function to openApplication string * critical-applications.component.html: replace drawer state with reactive observable patterns - Replace dataService.drawerInvokerId with drawerDetails$ | async in card highlighting - Update table component binding from isDrawerIsOpenForThisRecord to openApplication - Use reactive drawer state checking for consistent behavior with all-applications * all-applications.component.ts: remove deprecated drawer state functions - Remove unused trackByFunction that's no longer needed in template - Remove getSelectedUrls function that's not used anywhere - Remove isDrawerOpenForTableRow replaced by reactive openApplication binding - Clean up unused ApplicationHealthReportDetail import - Simplifies component interface following reactive pattern migration * critical-applications.component.ts: remove deprecated drawer state functions - Remove unused trackByFunction that's no longer needed in template - Remove isDrawerOpenForTableRow replaced by reactive openApplication binding * app-table-row-scrollable.component.html: replace drawer function calls with string comparison - Replace isDrawerIsOpenForThisRecord(row.applicationName) with row.applicationName === openApplication - Use direct string comparison instead of function calls for better performance - Matches updated component input from function to string property - Simplifies template logic following reactive pattern migration * fix(risk-insights-data.service.ts): restore drawer toggle behavior in setter methods - Add toggle logic to check if same drawer type and invoker are already open - Close drawer when clicking same button twice (preserves original UX) - Switch drawer content when clicking different button - Maintains reactive patterns while restoring expected behavior * revert to drawer state functions to maintain scope of task - the logic replacing these functions will be in pr16523 * fix(risk-insights-data.service.ts): restore boolean isActiveDrawerType function per review feedback - Keep original isActiveDrawerType() as boolean function using drawerDetailsSubject.value - Maintain isActiveDrawerType$() as Observable version for reactive templates - Apply same pattern to isDrawerOpenForInvoker() for consistency - Addresses review feedback to preserve existing function signatures * refactor(risk-insights-data.service.ts): use destructuring in drawer setter methods per review feedback * refactor(all-applications.component.html): optimize single subscription for drawer state per review feedback * refactor(critical-applications.component.html): optimize single subscription for drawer state per review feedback * refactor(risk-insights.component.html): use boolean drawer type functions per review feedback * fix(browser-system-notification.service.ts): restore eslint disable comment removed by prettier --------- Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com> --- .../services/risk-insights-data.service.ts | 120 +++++++++---- .../all-applications.component.html | 74 ++++---- .../all-applications.component.ts | 15 +- .../critical-applications.component.html | 74 ++++---- .../critical-applications.component.ts | 7 +- .../risk-insights.component.html | 164 +++++++++--------- .../risk-insights.component.ts | 34 +++- 7 files changed, 280 insertions(+), 208 deletions(-) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts index f58c13a9cfb..cc90fb6940a 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts @@ -1,5 +1,5 @@ import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; -import { finalize, switchMap, withLatestFrom } from "rxjs/operators"; +import { finalize, switchMap, withLatestFrom, map } from "rxjs/operators"; import { getOrganizationById, @@ -14,6 +14,7 @@ import { AtRiskApplicationDetail, AtRiskMemberDetail, DrawerType, + DrawerDetails, ApplicationHealthReportDetail, ApplicationHealthReportDetailEnriched, } from "../models/report-models"; @@ -53,12 +54,17 @@ export class RiskInsightsDataService { private errorSubject = new BehaviorSubject(null); error$ = this.errorSubject.asObservable(); - openDrawer = false; - drawerInvokerId: string = ""; - activeDrawerType: DrawerType = DrawerType.None; - atRiskMemberDetails: AtRiskMemberDetail[] = []; - appAtRiskMembers: AppAtRiskMembersDialogParams | null = null; - atRiskAppDetails: AtRiskApplicationDetail[] | null = null; + // ------------------------- Drawer Variables ---------------- + // Drawer variables unified into a single BehaviorSubject + private drawerDetailsSubject = new BehaviorSubject({ + open: false, + invokerId: "", + activeDrawerType: DrawerType.None, + atRiskMemberDetails: [], + appAtRiskMembers: null, + atRiskAppDetails: null, + }); + drawerDetails$ = this.drawerDetailsSubject.asObservable(); constructor( private accountService: AccountService, @@ -178,56 +184,96 @@ export class RiskInsightsDataService { } // ------------------------------- Drawer management methods ------------------------------- + // ------------------------- Drawer functions ----------------------------- + + isActiveDrawerType$ = (drawerType: DrawerType): Observable => { + return this.drawerDetails$.pipe(map((details) => details.activeDrawerType === drawerType)); + }; isActiveDrawerType = (drawerType: DrawerType): boolean => { - return this.activeDrawerType === drawerType; + return this.drawerDetailsSubject.value.activeDrawerType === drawerType; + }; + + isDrawerOpenForInvoker$ = (applicationName: string) => { + return this.drawerDetails$.pipe(map((details) => details.invokerId === applicationName)); + }; + isDrawerOpenForInvoker = (applicationName: string): boolean => { + return this.drawerDetailsSubject.value.invokerId === applicationName; + }; + + closeDrawer = (): void => { + this.drawerDetailsSubject.next({ + open: false, + invokerId: "", + activeDrawerType: DrawerType.None, + atRiskMemberDetails: [], + appAtRiskMembers: null, + atRiskAppDetails: null, + }); }; setDrawerForOrgAtRiskMembers = ( atRiskMemberDetails: AtRiskMemberDetail[], invokerId: string = "", ): void => { - this.resetDrawer(DrawerType.OrgAtRiskMembers); - this.activeDrawerType = DrawerType.OrgAtRiskMembers; - this.drawerInvokerId = invokerId; - this.atRiskMemberDetails = atRiskMemberDetails; - this.openDrawer = !this.openDrawer; + const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value; + const shouldClose = + open && activeDrawerType === DrawerType.OrgAtRiskMembers && currentInvokerId === invokerId; + + if (shouldClose) { + this.closeDrawer(); + } else { + this.drawerDetailsSubject.next({ + open: true, + invokerId, + activeDrawerType: DrawerType.OrgAtRiskMembers, + atRiskMemberDetails, + appAtRiskMembers: null, + atRiskAppDetails: null, + }); + } }; setDrawerForAppAtRiskMembers = ( atRiskMembersDialogParams: AppAtRiskMembersDialogParams, invokerId: string = "", ): void => { - this.resetDrawer(DrawerType.None); - this.activeDrawerType = DrawerType.AppAtRiskMembers; - this.drawerInvokerId = invokerId; - this.appAtRiskMembers = atRiskMembersDialogParams; - this.openDrawer = !this.openDrawer; + const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value; + const shouldClose = + open && activeDrawerType === DrawerType.AppAtRiskMembers && currentInvokerId === invokerId; + + if (shouldClose) { + this.closeDrawer(); + } else { + this.drawerDetailsSubject.next({ + open: true, + invokerId, + activeDrawerType: DrawerType.AppAtRiskMembers, + atRiskMemberDetails: [], + appAtRiskMembers: atRiskMembersDialogParams, + atRiskAppDetails: null, + }); + } }; setDrawerForOrgAtRiskApps = ( atRiskApps: AtRiskApplicationDetail[], invokerId: string = "", ): void => { - this.resetDrawer(DrawerType.OrgAtRiskApps); - this.activeDrawerType = DrawerType.OrgAtRiskApps; - this.drawerInvokerId = invokerId; - this.atRiskAppDetails = atRiskApps; - this.openDrawer = !this.openDrawer; - }; + const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value; + const shouldClose = + open && activeDrawerType === DrawerType.OrgAtRiskApps && currentInvokerId === invokerId; - closeDrawer = (): void => { - this.resetDrawer(DrawerType.None); - }; - - private resetDrawer = (drawerType: DrawerType): void => { - if (this.activeDrawerType !== drawerType) { - this.openDrawer = false; + if (shouldClose) { + this.closeDrawer(); + } else { + this.drawerDetailsSubject.next({ + open: true, + invokerId, + activeDrawerType: DrawerType.OrgAtRiskApps, + atRiskMemberDetails: [], + appAtRiskMembers: null, + atRiskAppDetails: atRiskApps, + }); } - - this.activeDrawerType = DrawerType.None; - this.atRiskMemberDetails = []; - this.appAtRiskMembers = null; - this.atRiskAppDetails = null; - this.drawerInvokerId = ""; }; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html index 79f18803727..b861925f7cb 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html @@ -25,44 +25,46 @@

{{ "allApplications" | i18n }}

-
- - + -
+ + + +
+ }
{ const info = { members: @@ -226,9 +219,9 @@ export class AllApplicationsComponent implements OnInit { } }; - getSelectedUrls = () => Array.from(this.selectedUrls); - isDrawerOpenForTableRow = (applicationName: string): boolean => { - return this.dataService.drawerInvokerId === applicationName; + // Note: This function will be replaced by PR #16523 with openApplication binding + // Using private access to BehaviorSubject value for backward compatibility + return (this.dataService as any).drawerDetailsSubject?.value?.invokerId === applicationName; }; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html index f78880b34a0..18b114515af 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html @@ -43,44 +43,46 @@ }}
-
- - + -
+ + + + + }
{ - return this.dataService.drawerInvokerId === applicationName; + // Note: This function will be replaced by PR #16523 with openApplication binding + // Using private access to BehaviorSubject value for backward compatibility + return (this.dataService as any).drawerDetailsSubject?.value?.invokerId === applicationName; }; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index 3c2d5cd0a15..50af2c9e9a7 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -53,93 +53,91 @@ - - - - - - {{ - (dataService.atRiskMemberDetails.length > 0 - ? "atRiskMembersDescription" - : "atRiskMembersDescriptionNone" - ) | i18n - }} - -
-
{{ "email" | i18n }}
-
- {{ "atRiskPasswords" | i18n }} + @if (dataService.drawerDetails$ | async; as drawerDetails) { + + + + + + {{ + (drawerDetails.atRiskMemberDetails.length > 0 + ? "atRiskMembersDescription" + : "atRiskMembersDescriptionNone" + ) | i18n + }} + +
+
{{ "email" | i18n }}
+
+ {{ "atRiskPasswords" | i18n }} +
+ +
+
{{ member.email }}
+
{{ member.atRiskPasswordCount }}
+
+
+
+
+
+ + @if (dataService.isActiveDrawerType(drawerTypes.AppAtRiskMembers)) { + + + +
+ {{ "atRiskMembersWithCount" | i18n: drawerDetails.appAtRiskMembers.members.length }}
- -
+
+ {{ + (drawerDetails.appAtRiskMembers.members.length > 0 + ? "atRiskMembersDescriptionWithApp" + : "atRiskMembersDescriptionWithAppNone" + ) | i18n: drawerDetails.appAtRiskMembers.applicationName + }} +
+
+
{{ member.email }}
-
{{ member.atRiskPasswordCount }}
-
- - - - - - - - - -
- {{ "atRiskMembersWithCount" | i18n: dataService.appAtRiskMembers.members.length }} -
-
- {{ - (dataService.appAtRiskMembers.members.length > 0 - ? "atRiskMembersDescriptionWithApp" - : "atRiskMembersDescriptionWithAppNone" - ) | i18n: dataService.appAtRiskMembers.applicationName - }} -
-
- -
{{ member.email }}
-
-
-
-
- - - - - - - {{ - (dataService.atRiskAppDetails.length > 0 - ? "atRiskApplicationsDescription" - : "atRiskApplicationsDescriptionNone" - ) | i18n - }} - -
-
- {{ "application" | i18n }} -
-
- {{ "atRiskPasswords" | i18n }} -
+
- -
-
{{ app.applicationName }}
-
{{ app.atRiskPasswordCount }}
+ + } + + @if (dataService.isActiveDrawerType(drawerTypes.OrgAtRiskApps)) { + + + + + {{ + (drawerDetails.atRiskAppDetails.length > 0 + ? "atRiskApplicationsDescription" + : "atRiskApplicationsDescriptionNone" + ) | i18n + }} + +
+
+ {{ "application" | i18n }} +
+
+ {{ "atRiskPasswords" | i18n }} +
+ +
+
{{ app.applicationName }}
+
{{ app.atRiskPasswordCount }}
+
+
- -
- - + + } + + } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index 8279ae612e9..a34cae44f14 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -61,6 +61,9 @@ export enum RiskInsightsTabType { ], }) export class RiskInsightsComponent implements OnInit { + private destroyRef = inject(DestroyRef); + private _isDrawerOpen: boolean = false; + tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllApps; isRiskInsightsActivityTabFeatureEnabled: boolean = false; @@ -73,7 +76,6 @@ export class RiskInsightsComponent implements OnInit { notifiedMembersCount: number = 0; private organizationId: OrganizationId = "" as OrganizationId; - private destroyRef = inject(DestroyRef); isLoading$: Observable = new Observable(); isRefreshing$: Observable = new Observable(); @@ -136,6 +138,13 @@ export class RiskInsightsComponent implements OnInit { ); }, }); + + // Subscribe to drawer state changes + this.dataService.drawerDetails$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((details) => { + this._isDrawerOpen = details.open; + }); } /** @@ -163,4 +172,27 @@ export class RiskInsightsComponent implements OnInit { get drawerTypes(): typeof DrawerType { return DrawerType; } + + /** + * Special case getter for syncing drawer state from service to component. + * This allows the template to use two-way binding while staying reactive. + */ + get isDrawerOpen() { + return this._isDrawerOpen; + } + + /** + * Special case setter for syncing drawer state from component to service. + * When the drawer component closes the drawer, this syncs the state back to the service. + */ + set isDrawerOpen(value: boolean) { + if (this._isDrawerOpen !== value) { + this._isDrawerOpen = value; + + // Close the drawer in the service if the drawer component closed the drawer + if (!value) { + this.dataService.closeDrawer(); + } + } + } } From 99d7097378265db56aaecd3ba14588e14b1d42e8 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 08:53:04 +0000 Subject: [PATCH 17/55] Autosync the updated translations (#16637) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/az/messages.json | 18 +++++++-------- apps/desktop/src/locales/fr/messages.json | 22 +++++++++--------- apps/desktop/src/locales/ru/messages.json | 2 +- apps/desktop/src/locales/sr/messages.json | 18 +++++++-------- apps/desktop/src/locales/sv/messages.json | 6 ++--- apps/desktop/src/locales/uk/messages.json | 2 +- apps/desktop/src/locales/vi/messages.json | 24 ++++++++++---------- apps/desktop/src/locales/zh_CN/messages.json | 4 ++-- 8 files changed, 48 insertions(+), 48 deletions(-) diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 4b6a3cfa416..9618bafca3e 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -4109,30 +4109,30 @@ "message": "Qısayola düzəliş et" }, "archive": { - "message": "Archive" + "message": "Arxivlə" }, "unarchive": { - "message": "Unarchive" + "message": "Arxivdən çıxart" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Arxivdəki elementlər" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Arxivdə element yoxdur" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Element arxivə göndərildi" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Element arxivdən çıxarıldı" }, "archiveItem": { - "message": "Archive item" + "message": "Elementi arxivlə" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Arxivlənmiş elementlər ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək. Bu elementi arxivləmək istədiyinizə əminsiniz?" } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index dee70397307..f85708edbe0 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -4081,7 +4081,7 @@ "message": "Afficher moins" }, "enableAutotype": { - "message": "Activer le type automatique" + "message": "Activer la Saisie Auto" }, "enableAutotypeDescription": { "message": "Bitwarden ne valide pas les emplacements d'entrée, assurez-vous d'être dans la bonne fenêtre et le bon champ avant d'utiliser le raccourci." @@ -4100,10 +4100,10 @@ "message": "Confirmer" }, "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "message": "Activer le raccourci de la Saisie Auto" }, "enableAutotypeDescriptionTransitionKey": { - "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." + "message": "Assurez-vous d'être dans le bon champ avant d'utiliser le raccourci pour éviter de remplir les données au mauvais endroit." }, "editShortcut": { "message": "Modifier le raccourci" @@ -4112,27 +4112,27 @@ "message": "Archive" }, "unarchive": { - "message": "Unarchive" + "message": "Désarchiver" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Éléments dans l'archive" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Aucun élément dans l'archive" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Élément envoyé à l'archive" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Élément retiré de l'archive" }, "archiveItem": { - "message": "Archive item" + "message": "Archiver l'élément" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Les éléments archivés sont exclus des résultats de recherche généraux et des suggestions de remplissage automatique. Êtes-vous sûr de vouloir archiver cet élément ?" } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 10c29017c46..292564be5f3 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -4087,7 +4087,7 @@ "message": "Bitwarden не проверяет местоположение ввода, поэтому, прежде чем использовать ярлык, убедитесь, что вы находитесь в нужном окне и поле." }, "moreBreadcrumbs": { - "message": "More breadcrumbs", + "message": "Больше хлебных крошек", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." }, "next": { diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index ca7cb763304..cd41aa9e4b9 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -4109,30 +4109,30 @@ "message": "Уреди пречицу" }, "archive": { - "message": "Archive" + "message": "Архива" }, "unarchive": { - "message": "Unarchive" + "message": "Врати из архиве" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Ставке у архиви" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Нема ставка у архиви" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Ставка је послата у архиву" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Ставка је уклоњена из архиве" }, "archiveItem": { - "message": "Archive item" + "message": "Архивирај ставку" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Архивиране ставке су искључене из општих резултата претраге и предлога за ауто попуњавање. Јесте ли сигурни да желите да архивирате ову ставку?" } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index e3cbba09d6b..a867fc28753 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -4121,18 +4121,18 @@ "message": "Inga objekt i arkivet" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Arkiverade objekt kommer att visas här och kommer att uteslutas från allmänna sökresultat och förslag för autofyll." }, "itemSentToArchive": { "message": "Objekt skickat till arkiv" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Objekt borttaget från arkiv" }, "archiveItem": { "message": "Arkivera objekt" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Arkiverade objekt är uteslutna från allmänna sökresultat och förslag för autofyll. Är du säker på att du vill arkivera detta objekt?" } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index a9885566fd0..c5c86baacdf 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2751,7 +2751,7 @@ "message": "Адреса е-пошти Catch-all" }, "catchallEmailDesc": { - "message": "Використовуйте свою скриньку вхідних Catch-All власного домену." + "message": "Використовуйте можливості Catch-All власного домену." }, "useThisEmail": { "message": "Використати цю е-пошту" diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 6613e983919..a9ac6aa5bd7 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -1303,7 +1303,7 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Hiển thị biểu tượng trang web và truy xuất các URL đổi mật khẩu" }, "enableMinToTray": { "message": "Thu nhỏ vào khay hệ thống" @@ -3935,10 +3935,10 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "aboutThisSetting": { - "message": "About this setting" + "message": "Về cài đặt này" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden sẽ sử dụng URI đăng nhập đã lưu để xác định biểu tượng hoặc URL đổi mật khẩu nào cần dùng nhằm cải thiện trải nghiệm của bạn. Không có thông tin nào được thu thập hay lưu lại khi bạn sử dụng dịch vụ này." }, "assignToCollections": { "message": "Gán vào bộ sưu tập" @@ -4109,30 +4109,30 @@ "message": "Chỉnh sửa phím tắt" }, "archive": { - "message": "Archive" + "message": "Lưu trữ" }, "unarchive": { - "message": "Unarchive" + "message": "Hủy lưu trữ" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Các mục trong kho lưu trữ" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Không có mục nào trong kho lưu trữ" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Mục đã được gửi đến kho lưu trữ" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Mục đã được gỡ khỏi kho lưu trữ" }, "archiveItem": { - "message": "Archive item" + "message": "Lưu trữ mục" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Các mục đã lưu trữ sẽ bị loại khỏi kết quả tìm kiếm chung và gợi ý tự động điền. Bạn có chắc chắn muốn lưu trữ mục này không?" } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 102df7e9a27..552afdca34c 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1363,7 +1363,7 @@ "message": "确认隐藏到托盘" }, "confirmTrayDesc": { - "message": "关闭此设置也将关闭其他与托盘相关的设置。" + "message": "停用此设置也将停用其他与托盘相关的设置。" }, "language": { "message": "语言" @@ -4081,7 +4081,7 @@ "message": "显示更少" }, "enableAutotype": { - "message": "启用自动填写" + "message": "启用自动输入" }, "enableAutotypeDescription": { "message": "Bitwarden 不会验证输入位置,在使用快捷键之前,请确保您位于正确的窗口和字段中。" From b888274bd0515527794714aff999dc84f03fb5cf Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 08:53:27 +0000 Subject: [PATCH 18/55] Autosync the updated translations (#16638) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/az/messages.json | 18 ++--- apps/browser/src/_locales/sr/messages.json | 18 ++--- apps/browser/src/_locales/uk/messages.json | 80 +++++++++---------- apps/browser/src/_locales/vi/messages.json | 70 ++++++++-------- apps/browser/src/_locales/zh_CN/messages.json | 6 +- apps/browser/store/locales/uk/copy.resx | 8 +- 6 files changed, 100 insertions(+), 100 deletions(-) diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index c5a688f152f..639e6c87d36 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -551,31 +551,31 @@ "message": "Axtarışı sıfırla" }, "archive": { - "message": "Archive" + "message": "Arxivlə" }, "unarchive": { - "message": "Unarchive" + "message": "Arxivdən çıxart" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Arxivdəki elementlər" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Arxivdə element yoxdur" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Element arxivə göndərildi" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Element arxivdən çıxarıldı" }, "archiveItem": { - "message": "Archive item" + "message": "Elementi arxivlə" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Arxivlənmiş elementlər ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək. Bu elementi arxivləmək istədiyinizə əminsiniz?" }, "edit": { "message": "Düzəliş et" diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index cc4abafe878..269ebd41bdd 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -551,31 +551,31 @@ "message": "Ресетовати претрагу" }, "archive": { - "message": "Archive" + "message": "Архива" }, "unarchive": { - "message": "Unarchive" + "message": "Врати из архиве" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Ставке у архиви" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Нема ставка у архиви" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Ставка је послата у архиву" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Ставка је уклоњена из архиве" }, "archiveItem": { - "message": "Archive item" + "message": "Архивирај ставку" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Архивиране ставке су искључене из општих резултата претраге и предлога за ауто попуњавање. Јесте ли сигурни да желите да архивирате ову ставку?" }, "edit": { "message": "Уреди" diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 850c174f666..f088e610051 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -551,31 +551,31 @@ "message": "Скинути пошук" }, "archive": { - "message": "Archive" + "message": "Архівувати" }, "unarchive": { - "message": "Unarchive" + "message": "Видобути" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Записи в архіві" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Немає записів у архіві" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Архівовані записи з'являтимуться тут і будуть виключені з результатів звичайного пошуку та пропозицій автозаповнення." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Запис переміщено до архіву" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Запис вилучено з архіву" }, "archiveItem": { - "message": "Archive item" + "message": "Архівувати запис" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Архівовані записи виключаються з результатів звичайного пошуку та пропозицій автозаповнення. Ви дійсно хочете архівувати цей запис?" }, "edit": { "message": "Змінити" @@ -584,7 +584,7 @@ "message": "Переглянути" }, "viewLogin": { - "message": "View login" + "message": "Переглянути запис" }, "noItemsInList": { "message": "Немає записів." @@ -1782,7 +1782,7 @@ "message": "Натискання поза межами спливаючого вікна для перевірки коду підтвердження в пошті спричинить його закриття. Хочете відкрити його в новому вікні, щоб воно не закрилося?" }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Показувати піктограми вебсайтів та отримувати адреси для зміни паролів" }, "cardholderName": { "message": "Ім'я власника картки" @@ -1947,79 +1947,79 @@ "message": "Нотатка" }, "newItemHeaderLogin": { - "message": "New Login", + "message": "Новий запис", "description": "Header for new login item type" }, "newItemHeaderCard": { - "message": "New Card", + "message": "Нова картка", "description": "Header for new card item type" }, "newItemHeaderIdentity": { - "message": "New Identity", + "message": "Нове посвідчення", "description": "Header for new identity item type" }, "newItemHeaderNote": { - "message": "New Note", + "message": "Нова нотатка", "description": "Header for new note item type" }, "newItemHeaderSshKey": { - "message": "New SSH key", + "message": "Новий ключ SSH", "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "New Text Send", + "message": "Нове текстове відправлення", "description": "Header for new text send" }, "newItemHeaderFileSend": { - "message": "New File Send", + "message": "Нове файлове відправлення", "description": "Header for new file send" }, "editItemHeaderLogin": { - "message": "Edit Login", + "message": "Редагувати запис", "description": "Header for edit login item type" }, "editItemHeaderCard": { - "message": "Edit Card", + "message": "Редагувати картку", "description": "Header for edit card item type" }, "editItemHeaderIdentity": { - "message": "Edit Identity", + "message": "Редагувати посвідчення", "description": "Header for edit identity item type" }, "editItemHeaderNote": { - "message": "Edit Note", + "message": "Редагувати нотатку", "description": "Header for edit note item type" }, "editItemHeaderSshKey": { - "message": "Edit SSH key", + "message": "Редагувати ключ SSH", "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Edit Text Send", + "message": "Редагувати текстове відправлення", "description": "Header for edit text send" }, "editItemHeaderFileSend": { - "message": "Edit File Send", + "message": "Редагувати файлове відправлення", "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "Перегляд запису", "description": "Header for view login item type" }, "viewItemHeaderCard": { - "message": "View Card", + "message": "Перегляд картки", "description": "Header for view card item type" }, "viewItemHeaderIdentity": { - "message": "View Identity", + "message": "Перегляд посвідчення", "description": "Header for view identity item type" }, "viewItemHeaderNote": { - "message": "View Note", + "message": "Перегляд нотатки", "description": "Header for view note item type" }, "viewItemHeaderSshKey": { - "message": "View SSH key", + "message": "Перегляд ключа SSH", "description": "Header for view SSH key item type" }, "passwordHistory": { @@ -3261,7 +3261,7 @@ "message": "Адреса е-пошти Catch-all" }, "catchallEmailDesc": { - "message": "Використовуйте свою скриньку вхідних Catch-All власного домену." + "message": "Використовуйте можливості Catch-All власного домену." }, "random": { "message": "Випадково" @@ -5485,10 +5485,10 @@ "message": "Змінити ризикований пароль" }, "changeAtRiskPasswordAndAddWebsite": { - "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." + "message": "Цей запис ризикований, і не має адреси вебсайту. Додайте адресу вебсайту і змініть пароль для вдосконалення безпеки." }, "missingWebsite": { - "message": "Missing website" + "message": "Немає вебсайту" }, "settingsVaultOptions": { "message": "Параметри сховища" @@ -5539,16 +5539,16 @@ "message": "Вітаємо у вашому сховищі!" }, "phishingPageTitle": { - "message": "Phishing website" + "message": "Шахрайський вебсайт" }, "phishingPageCloseTab": { - "message": "Close tab" + "message": "Закрити вкладку" }, "phishingPageContinue": { - "message": "Continue" + "message": "Продовжити" }, "phishingPageLearnWhy": { - "message": "Why are you seeing this?" + "message": "Чому ви це бачите?" }, "hasItemsVaultNudgeBodyOne": { "message": "Автозаповнення записів для поточної сторінки" @@ -5626,10 +5626,10 @@ "description": "Aria label for the body content of the generator nudge" }, "aboutThisSetting": { - "message": "About this setting" + "message": "Про ці налаштування" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden використовуватиме збережені URI-адреси записів для визначення піктограм вебсайтів або URL-адрес для зміни паролів, щоб вдосконалити вашу роботу. Під час використання цієї послуги ваша інформація не збирається і не зберігається." }, "noPermissionsViewPage": { "message": "У вас немає дозволу переглядати цю сторінку. Спробуйте ввійти з іншим обліковим записом." @@ -5652,6 +5652,6 @@ "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." }, "confirmKeyConnectorDomain": { - "message": "Confirm Key Connector domain" + "message": "Підтвердити домен Key Connector" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 76bea4120cd..06920433037 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -551,31 +551,31 @@ "message": "Đặt lại tìm kiếm" }, "archive": { - "message": "Archive" + "message": "Lưu trữ" }, "unarchive": { - "message": "Unarchive" + "message": "Hủy lưu trữ" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Các mục trong kho lưu trữ" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Không có mục nào trong kho lưu trữ" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Mục đã được gửi đến kho lưu trữ" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Mục đã được gỡ khỏi kho lưu trữ" }, "archiveItem": { - "message": "Archive item" + "message": "Lưu trữ mục" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Các mục đã lưu trữ sẽ bị loại khỏi kết quả tìm kiếm chung và gợi ý tự động điền. Bạn có chắc chắn muốn lưu trữ mục này không?" }, "edit": { "message": "Sửa" @@ -1782,7 +1782,7 @@ "message": "Nhấp bên ngoài popup để xem mã xác thực trong email của bạn sẽ làm cho popup này đóng lại. Bạn có muốn mở popup này trong một cửa sổ mới để nó không bị đóng?" }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Hiển thị biểu tượng trang web và truy xuất các URL đổi mật khẩu" }, "cardholderName": { "message": "Tên chủ thẻ" @@ -1947,79 +1947,79 @@ "message": "Ghi chú" }, "newItemHeaderLogin": { - "message": "New Login", + "message": "Đăng nhập mới", "description": "Header for new login item type" }, "newItemHeaderCard": { - "message": "New Card", + "message": "Thẻ mới", "description": "Header for new card item type" }, "newItemHeaderIdentity": { - "message": "New Identity", + "message": "Danh tính mới", "description": "Header for new identity item type" }, "newItemHeaderNote": { - "message": "New Note", + "message": "Ghi chú mới", "description": "Header for new note item type" }, "newItemHeaderSshKey": { - "message": "New SSH key", + "message": "Khóa SSH mới", "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "New Text Send", + "message": "Send văn bản mới", "description": "Header for new text send" }, "newItemHeaderFileSend": { - "message": "New File Send", + "message": "Send tập tin mới", "description": "Header for new file send" }, "editItemHeaderLogin": { - "message": "Edit Login", + "message": "Chỉnh sửa đăng nhập", "description": "Header for edit login item type" }, "editItemHeaderCard": { - "message": "Edit Card", + "message": "Chỉnh sửa thẻ", "description": "Header for edit card item type" }, "editItemHeaderIdentity": { - "message": "Edit Identity", + "message": "Chỉnh sửa danh tính", "description": "Header for edit identity item type" }, "editItemHeaderNote": { - "message": "Edit Note", + "message": "Chỉnh sửa ghi chú", "description": "Header for edit note item type" }, "editItemHeaderSshKey": { - "message": "Edit SSH key", + "message": "Sửa khóa SSH", "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Edit Text Send", + "message": "Sửa Send văn bản", "description": "Header for edit text send" }, "editItemHeaderFileSend": { - "message": "Edit File Send", + "message": "Sửa Send tập tin", "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "Xem đăng nhập", "description": "Header for view login item type" }, "viewItemHeaderCard": { - "message": "View Card", + "message": "Xem thẻ", "description": "Header for view card item type" }, "viewItemHeaderIdentity": { - "message": "View Identity", + "message": "Xem danh tính", "description": "Header for view identity item type" }, "viewItemHeaderNote": { - "message": "View Note", + "message": "Xem ghi chú", "description": "Header for view note item type" }, "viewItemHeaderSshKey": { - "message": "View SSH key", + "message": "Xem khóa SSH", "description": "Header for view SSH key item type" }, "passwordHistory": { @@ -5539,16 +5539,16 @@ "message": "Chào mừng đến với kho lưu trữ của bạn!" }, "phishingPageTitle": { - "message": "Phishing website" + "message": "Trang web lừa đảo" }, "phishingPageCloseTab": { - "message": "Close tab" + "message": "Đóng tab" }, "phishingPageContinue": { - "message": "Continue" + "message": "Tiếp tục" }, "phishingPageLearnWhy": { - "message": "Why are you seeing this?" + "message": "Tại sao bạn thấy điều này?" }, "hasItemsVaultNudgeBodyOne": { "message": "Tự động điền các mục cho trang hiện tại" @@ -5626,10 +5626,10 @@ "description": "Aria label for the body content of the generator nudge" }, "aboutThisSetting": { - "message": "About this setting" + "message": "Về cài đặt này" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden sẽ sử dụng URI đăng nhập đã lưu để xác định biểu tượng hoặc URL đổi mật khẩu nào cần dùng nhằm cải thiện trải nghiệm của bạn. Không có thông tin nào được thu thập hay lưu lại khi bạn sử dụng dịch vụ này." }, "noPermissionsViewPage": { "message": "Bạn không có quyền truy cập vào trang này. Hãy thử đăng nhập bằng tài khoản khác." diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 051914b120c..eb3cf1aa901 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1639,10 +1639,10 @@ "message": "轻松找到自动填充建议" }, "autofillSpotlightDesc": { - "message": "关闭浏览器的自动填充设置,以免与 Bitwarden 产生冲突。" + "message": "停用您浏览器的自动填充设置,以免与 Bitwarden 产生冲突。" }, "turnOffBrowserAutofill": { - "message": "关闭 $BROWSER$ 的自动填充", + "message": "停用 $BROWSER$ 自动填充", "placeholders": { "browser": { "content": "$1", @@ -1651,7 +1651,7 @@ } }, "turnOffAutofill": { - "message": "关闭自动填充" + "message": "停用自动填充" }, "showInlineMenuLabel": { "message": "在表单字段中显示自动填充建议" diff --git a/apps/browser/store/locales/uk/copy.resx b/apps/browser/store/locales/uk/copy.resx index 6f994cd0bf1..0bc1e7afce4 100644 --- a/apps/browser/store/locales/uk/copy.resx +++ b/apps/browser/store/locales/uk/copy.resx @@ -130,7 +130,7 @@ Убезпечте своє цифрове життя та захистіться від витоків даних, генеруючи та зберігаючи унікальні надійні паролі для кожного облікового запису. Зберігайте все в наскрізно зашифрованому сховищі паролів, доступ до якого маєте тільки ви. ДОСТУП ДО ДАНИХ БУДЬ-ДЕ, БУДЬ-КОЛИ, НА БУДЬ-ЯКОМУ ПРИСТРОЇ -Легко керуйте, зберігайте, захищайте та діліться необмеженою кількістю паролів на необмеженій кількості пристроїв. +Легко зберігайте необмежену кількість паролів у безпеці на необмеженій кількості пристроїв, а також керуйте й діліться ними. КОЖЕН ПОВИНЕН МАТИ ІНСТРУМЕНТИ ДЛЯ БЕЗПЕКИ В ІНТЕРНЕТІ Використовуйте Bitwarden безплатно без реклами або продажу даних. Bitwarden вважає, що кожен повинен мати можливість залишатися в безпеці в Інтернеті. Завдяки тарифним планам Преміум можна отримати доступ до розширених можливостей. @@ -144,7 +144,7 @@ Інші причини для вибору Bitwarden: Шифрування світового класу -Паролі захищаються розширеним наскрізним шифруванням (AES-256, сіллю хешування і PBKDF2 SHA-256), тому ваші дані завжди зберігаються приватно і в безпеці. +Паролі захищаються за допомогою досконалого наскрізного шифрування (AES-256, хешуванням із сіллю і PBKDF2 SHA-256), тому ваші дані завжди зберігаються приватно і в безпеці. Сторонні аудити Bitwarden регулярно проводить комплексні аудити безпеки із залученням третіх сторін – відомих компаній у сфері безпеки. Під час цих щорічних аудитів проводиться оцінка програмного коду і тестування на проникнення через IP-адреси Bitwarden, сервери та вебпрограми. @@ -156,13 +156,13 @@ Bitwarden регулярно проводить комплексні аудит Передавайте дані безпосередньо іншим користувачам, зберігаючи наскрізне шифрування та обмежуючи їх викриття. Вбудований генератор -Створюйте довгі, складні та чіткі паролі, а також унікальні імена користувачів для кожного сайту, який ви відвідуєте. Користуйтеся інтеграцією з провайдерами псевдонімів електронної пошти для забезпечення додаткової приватності. +Створюйте довгі, складні та особливі паролі, а також унікальні імена користувачів для кожного сайту, який ви відвідуєте. Користуйтеся інтеграцією з провайдерами псевдонімів електронної пошти для забезпечення додаткової приватності. Переклад різними мовами Bitwarden перекладено понад 60 мовами завдяки зусиллям нашої світової спільноти на Crowdin. Програми для різних платформ -Захищайте та діліться конфіденційними даними в межах свого сховища Bitwarden з будь-якого браузера, мобільного пристрою або комп'ютерної ОС, а також інших можливостей. +Захищайте конфіденційні дані та діліться ними в межах свого сховища Bitwarden з будь-якого браузера, мобільного пристрою або комп'ютерної ОС, а також користуйтеся іншими можливостями. Bitwarden захищає не лише паролі Комплексні рішення для керування наскрізно зашифрованими обліковими даними від Bitwarden дають змогу організаціям захищати все, включно з секретами розробників та ключами доступу. Відвідайте Bitwarden.com, щоб дізнатися більше про Менеджер секретів Bitwarden і Bitwarden Passwordless.dev! From c1d749a0cfda9e457197e77ec0712f6971ffbc44 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 10:53:46 +0200 Subject: [PATCH 19/55] Autosync the updated translations (#16639) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/az/messages.json | 82 +++++------ apps/web/src/locales/bg/messages.json | 6 +- apps/web/src/locales/cs/messages.json | 4 +- apps/web/src/locales/fr/messages.json | 4 +- apps/web/src/locales/hu/messages.json | 4 +- apps/web/src/locales/lv/messages.json | 4 +- apps/web/src/locales/sk/messages.json | 4 +- apps/web/src/locales/sr_CY/messages.json | 178 +++++++++++------------ apps/web/src/locales/sv/messages.json | 2 +- apps/web/src/locales/uk/messages.json | 2 +- apps/web/src/locales/vi/messages.json | 144 +++++++++--------- apps/web/src/locales/zh_CN/messages.json | 12 +- 12 files changed, 223 insertions(+), 223 deletions(-) diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 8af382e2ad4..c9d53bd9c52 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -5558,10 +5558,10 @@ "message": "Müəssisə siyasətinə görə, elementləri şəxsi seyfinizdə saxlamağınız məhdudlaşdırılıb. Sahiblik seçimini təşkilat olaraq dəyişdirin və mövcud kolleksiyalar arasından seçim edin." }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "Masaüstü avto-yazma ilkin ayarı" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "Üzvlər üçün Masaüstü avto-yazma ayarını ilkin olaraq işə salın. Üzvlər Masaüstü client-ində avto-yazma ayarını manual olaraq söndürə bilər.", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { @@ -6729,7 +6729,7 @@ "message": "SSO sıradan çıxarılıb" }, "emailMustLoginWithSso": { - "message": "$EMAIL$ must login with Single Sign-on", + "message": "$EMAIL$, Vahid Daxil olma üsulu ilə giriş etməlidir", "placeholders": { "email": { "content": "$1", @@ -11022,16 +11022,16 @@ "message": "Ödənişsiz təşkilatların ən çox 2 kolleksiyası ola bilər. Daha çox kolleksiya əlavə etmək üçün ödənişli bir plana yüksəldin." }, "searchArchive": { - "message": "Search archive" + "message": "Arxivdə axtar" }, "archive": { - "message": "Archive" + "message": "Arxiv" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Arxivdə element yoxdur" }, "archivedItemsDescription": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, "businessUnit": { "message": "Biznes vahidi" @@ -11367,34 +11367,34 @@ "message": "İndi doğrula." }, "additionalStorageGB": { - "message": "Additional storage GB" + "message": "Əlavə anbar sahəsi GB" }, "additionalServiceAccountsV2": { - "message": "Additional machine accounts" + "message": "Əlavə maşın hesabları" }, "secretsManagerSeats": { - "message": "Secrets Manager seats" + "message": "Sirr Meneceri yerləri" }, "additionalStorage": { - "message": "Additional Storage" + "message": "Əlavə anbar sahəsi" }, "expandPurchaseDetails": { - "message": "Expand purchase details" + "message": "Satın alma detallarını genişləndir" }, "collapsePurchaseDetails": { - "message": "Collapse purchase details" + "message": "Satın alma detallarını yığcamlaşdır" }, "familiesMembership": { - "message": "Families membership" + "message": "Ailə üzvlüyü" }, "planDescPremium": { - "message": "Complete online security" + "message": "Tam onlayn təhlükəsizlik" }, "planDescFamiliesV2": { - "message": "Premium security for your family" + "message": "Ailəniz üçün Premium təhlükəsizlik" }, "planDescFreeV2": { - "message": "Share with $COUNT$ other user", + "message": "Digər $COUNT$ istifadəçi ilə paylaş", "placeholders": { "count": { "content": "$1", @@ -11403,37 +11403,37 @@ } }, "planDescEnterpriseV2": { - "message": "Advanced capabilities for any organization" + "message": "Hər bir təşkilat üçün qabaqcıl imkanlar" }, "planNameCustom": { - "message": "Custom plan" + "message": "Özəl plan" }, "planDescCustom": { - "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + "message": "Bitwarden, parolların və həssas məlumatların təhlükəsizliyini təmin etmək üçün istənilən miqyaslı şirkətə uyğunlaşır. Əgər böyük bir müəssisənin bir hissəsisinizsə, qiymət təklifi üçün satış şöbəsi ilə əlaqə saxlayın." }, "builtInAuthenticator": { - "message": "Built-in authenticator" + "message": "Daxili kimlik doğrulayıcı" }, "breachMonitoring": { - "message": "Breach monitoring" + "message": "Pozuntu monitorinqi" }, "andMoreFeatures": { - "message": "And more!" + "message": "Və daha çoxu!" }, "secureFileStorage": { - "message": "Secure file storage" + "message": "Güvənli fayl anbarı" }, "familiesUnlimitedSharing": { - "message": "Unlimited sharing - choose who sees what" + "message": "Limitsiz paylaşım - kimin nəyi görəcəyini seçin" }, "familiesUnlimitedCollections": { - "message": "Unlimited family collections" + "message": "Limitsiz ailə kolleksiyaları" }, "familiesSharedStorage": { - "message": "Shared storage for important family info" + "message": "Vacib ailə məlumatları üçün paylaşılan anbar sahəsi" }, "limitedUsersV2": { - "message": "Up to $COUNT$ members", + "message": "$COUNT$ üzvə qədər", "placeholders": { "count": { "content": "$1", @@ -11442,7 +11442,7 @@ } }, "limitedCollectionsV2": { - "message": "Up to $COUNT$ collections", + "message": "$COUNT$ kolleksiyaya qədər", "placeholders": { "count": { "content": "$1", @@ -11451,13 +11451,13 @@ } }, "alwaysFree": { - "message": "Always free" + "message": "Həmişə ödənişsiz" }, "twoSecretsIncluded": { - "message": "2 secrets" + "message": "2 sirr" }, "projectsIncludedV2": { - "message": "$COUNT$ project(s)", + "message": "$COUNT$ layihə", "placeholders": { "count": { "content": "$1", @@ -11466,13 +11466,13 @@ } }, "secureItemSharing": { - "message": "Secure item sharing" + "message": "Güvənli element paylaşımı" }, "scimSupport": { - "message": "SCIM support" + "message": "SCIM dəstəyi" }, "includedMachineAccountsV2": { - "message": "$COUNT$ machine accounts", + "message": "$COUNT$ maşın hesabı", "placeholders": { "count": { "content": "$1", @@ -11481,21 +11481,21 @@ } }, "enterpriseSecurityPolicies": { - "message": "Enterprise security policies" + "message": "Müəssisə təhlükəsizlik siyasətləri" }, "selfHostOption": { - "message": "Self-host option" + "message": "Self-host seçimi" }, "complimentaryFamiliesPlan": { - "message": "Complimentary families plan for all users" + "message": "Bütün istifadəçilər üçün ödənişsiz ailə planı" }, "strengthenCybersecurity": { - "message": "Strengthen cybersecurity" + "message": "Gücləndirilmiş kibertəhlükəsizlik" }, "boostProductivity": { - "message": "Boost productivity" + "message": "Məhsuldarlığı artırın" }, "seamlessIntegration": { - "message": "Seamless integration" + "message": "Problemsiz inteqrasiya" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index c1519a74b36..86136cb8f16 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -3580,7 +3580,7 @@ "description": "Browser extension/addon" }, "desktop": { - "message": "Десктоп", + "message": "Настолно приложение", "description": "Desktop app" }, "webVault": { @@ -5558,10 +5558,10 @@ "message": "Заради някоя политика за голяма организация не може да запазвате елементи в собствения си трезор. Променете собствеността да е на организация и изберете от наличните колекции." }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "Настройка по подразбиране за авт. попълване за работен плот" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "Включете автоматичното попълване за работен плот по подразбиране за всички членове. Всеки от тях ще може поотделно да изключи автоматичното попълване ръчно от настолното приложение.", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 576fe8b4d3f..5837d159e92 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -5558,10 +5558,10 @@ "message": "Z důvodu podnikových zásad nemůžete ukládat položky do svého osobního trezoru. Změňte vlastnictví položky na organizaci a poté si vyberte z dostupných sbírek." }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "Výchozí nastavení Desktop Autotype" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "Ve výchozím nastavení pro členy zapne Desktop Autotype. Uživatelé mohou v klientovi na počítači vypnout Desktop Autotype ručně.", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 2f056f4e384..e0c6cf2287d 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -5558,10 +5558,10 @@ "message": "En raison d'une politique de sécurité Entreprise, il vous est interdit d'enregistrer des éléments dans votre coffre personnel. Sélectionnez une organisation dans l'option Propriété et choisissez parmi les collections disponibles." }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "Réglage par Défaut de la Saisie Auto pour le Bureau" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "Activer la Saisie Auto pour le Bureau par défaut pour les membres. Les membres peuvent désactiver la Saisie Automatique manuellement dans l'application du Bureau.", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 4a3ae2eb194..910447f236c 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -5558,10 +5558,10 @@ "message": "Egy vállalati házirend miatt korlátozásra került az elemek személyes tárolóba történő mentése. Módosítsuk a Tulajdon opciót egy szervezetre és válasszunk az elérhető gyűjtemények közül." }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "Asztali automatikus típus aapértelmezett beállítás" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "Az asztali atumatikus típus alapértelmezés szerint bekapcsolásra kerül a tagok számára. A tagok manuálisan kikapcsolhatják az automatikus típus funkciót az Asztal kliensben.", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 568e23d45b7..bdf7dbef0fb 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -5558,10 +5558,10 @@ "message": "Uzņēmuma nosacījumi liedz saglabāt vienumus privātajā glabātavā. Ir jānorāda piederība apvienībai un jāizvēlas kāds no pieejamajiem krājumiem." }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "Darbvirsmas automātiskās ievades noklusējuma iestatījums" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "Pēc noklusējuma IESLĒGT dalībniekiem darbvirsmas automātisko aizpildi. Dalībnieki var pašrocīgi izslēgt automātisko aizpildi darbvirsmas klientā.", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index ab5864579d4..396d37e157e 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -5558,10 +5558,10 @@ "message": "Z dôvodu podnikovej politiky máte obmedzené ukladanie položiek do osobného trezora. Zmeňte možnosť vlastníctvo na organizáciu a vyberte si z dostupných zbierok." }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "Východzie nastavenie automatického vpisovania pre desktop" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "Vo východzom nastavení desktopu zapnúť členom automatické vpisovanie. Členovia si môžu automatické vpisovanie vypnúť v nastaveniach desktop klienta.", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index 4ad3b3723cf..68a440646a3 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -3,7 +3,7 @@ "message": "Све апликације" }, "activity": { - "message": "Activity" + "message": "Активност" }, "appLogoLabel": { "message": "Bitwarden лого" @@ -60,7 +60,7 @@ "message": "Креирајте нову ставку за пријаву" }, "criticalApplicationsActivityDescription": { - "message": "Once you mark applications critical, they will display here." + "message": "Апликације обележене као критичне, су приказане овде." }, "criticalApplicationsWithCount": { "message": "Критичне апликације ($COUNT$)", @@ -102,13 +102,13 @@ "message": "Док корисници чувају пријаве, апликације се појављују овде, приказујући све ризичне лозинке. Означите критичне апликације и обавестите кориснике да ажурирају лозинке." }, "noCriticalApplicationsTitle": { - "message": "You haven’t marked any applications as critical" + "message": "Нисте означили ниједну апликацију као критичну" }, "noCriticalApplicationsDescription": { - "message": "Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Изаберите своје најкритичније апликације да бисте приоритизовали безбедносне акције за своје кориснике и решили проблеме са угроженим лозинкама." }, "markCriticalApplications": { - "message": "Select critical applications" + "message": "Изаберите критичне апликације" }, "markAppAsCritical": { "message": "Означите апликацију као критичну" @@ -135,10 +135,10 @@ "message": "Чланови под ризиком" }, "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "message": "Чланови са правом уређивања за угрожене ставке критичних апликација" }, "membersAtRisk": { - "message": "$COUNT$ members at risk", + "message": "Угрожени чланови: $COUNT$", "placeholders": { "count": { "content": "$1", @@ -207,7 +207,7 @@ "message": "Уклони као критично" }, "criticalApplicationUnmarkedSuccessfully": { - "message": "Successfully unmarked application as critical" + "message": "Успешно неозначена апликација као критична" }, "whatTypeOfItem": { "message": "Који је ово тип елемента?" @@ -762,79 +762,79 @@ "message": "Види ставку" }, "newItemHeaderLogin": { - "message": "New Login", + "message": "Ново пријављивање", "description": "Header for new login item type" }, "newItemHeaderCard": { - "message": "New Card", + "message": "Нова картица", "description": "Header for new card item type" }, "newItemHeaderIdentity": { - "message": "New Identity", + "message": "Нови идентитет", "description": "Header for new identity item type" }, "newItemHeaderNote": { - "message": "New Note", + "message": "Нова белешка", "description": "Header for new note item type" }, "newItemHeaderSshKey": { - "message": "New SSH key", + "message": "Нов SSH кљич", "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "New Text Send", + "message": "Нови текст Send", "description": "Header for new text send" }, "newItemHeaderFileSend": { - "message": "New File Send", + "message": "Нова датотека Send", "description": "Header for new file send" }, "editItemHeaderLogin": { - "message": "Edit Login", + "message": "Уреди пријаву", "description": "Header for edit login item type" }, "editItemHeaderCard": { - "message": "Edit Card", + "message": "Уреди картицу", "description": "Header for edit card item type" }, "editItemHeaderIdentity": { - "message": "Edit Identity", + "message": "Уреди идентитет", "description": "Header for edit identity item type" }, "editItemHeaderNote": { - "message": "Edit Note", + "message": "Уреди белешку", "description": "Header for edit note item type" }, "editItemHeaderSshKey": { - "message": "Edit SSH key", + "message": "Уреди SSH кључ", "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Edit Text Send", + "message": "Уреди текст Send", "description": "Header for edit text send" }, "editItemHeaderFileSend": { - "message": "Edit File Send", + "message": "Уреди датотеку Send", "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "Преглед пријаве", "description": "Header for view login item type" }, "viewItemHeaderCard": { - "message": "View Card", + "message": "Преглед картице", "description": "Header for view card item type" }, "viewItemHeaderIdentity": { - "message": "View Identity", + "message": "Преглед идентитета", "description": "Header for view identity item type" }, "viewItemHeaderNote": { - "message": "View Note", + "message": "Преглед белешке", "description": "Header for view note item type" }, "viewItemHeaderSshKey": { - "message": "View SSH key", + "message": "Преглед SSH кључа", "description": "Header for view SSH key item type" }, "new": { @@ -2188,7 +2188,7 @@ "message": "Променити језик за Сеф." }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Прикажи иконе веб локација и преузмите линкове промене лозинке" }, "default": { "message": "Подразумевано" @@ -6528,7 +6528,7 @@ "message": "Нема породице чланова" }, "noMemberFamiliesDescription": { - "message": "Members who have redeemed family plans will display here" + "message": "Чланови који су искористили породичне планове биће приказани овде" }, "membersWithSponsoredFamilies": { "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." @@ -7520,7 +7520,7 @@ "message": "Искључено" }, "connected": { - "message": "Connected" + "message": "Повезано" }, "members": { "message": "Чланови" @@ -8495,7 +8495,7 @@ } }, "nameUnavailableProjectDeleted": { - "message": "Deleted project Id: $PROJECT_ID$", + "message": "Избрисан ИД пројекта: $PROJECT_ID$", "placeholders": { "project_id": { "content": "$1", @@ -8504,7 +8504,7 @@ } }, "nameUnavailableSecretDeleted": { - "message": "Deleted secret Id: $SECRET_ID$", + "message": "Избрисан ИД тајне: $SECRET_ID$", "placeholders": { "secret_id": { "content": "$1", @@ -8513,7 +8513,7 @@ } }, "editedProjectWithId": { - "message": "Edited a project with identifier: $PROJECT_ID$", + "message": "Уређен пројекат са ИД: $PROJECT_ID$", "placeholders": { "project_id": { "content": "$1", @@ -8522,7 +8522,7 @@ } }, "deletedProjectWithId": { - "message": "Deleted a project with identifier: $PROJECT_ID$", + "message": "Избрисан пројекат са ИД: $PROJECT_ID$", "placeholders": { "project_id": { "content": "$1", @@ -8531,7 +8531,7 @@ } }, "createdProjectWithId": { - "message": "Created a new project with identifier: $PROJECT_ID$", + "message": "Креирај нови пројекат са ИД: $PROJECT_ID$", "placeholders": { "project_id": { "content": "$1", @@ -8688,7 +8688,7 @@ "message": "Пријава је покренута" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Запамтити овај уређај да би будуће пријаве биле беспрекорне" }, "deviceApprovalRequired": { "message": "Потребно је одобрење уређаја. Изаберите опцију одобрења испод:" @@ -9783,7 +9783,7 @@ } }, "updateIntegrationButtonDesc": { - "message": "Update $INTEGRATION$", + "message": "Ажурирај $INTEGRATION$", "placeholders": { "integration": { "content": "$1", @@ -10708,7 +10708,7 @@ } }, "domainStatusClaimed": { - "message": "Claimed" + "message": "Захтевано" }, "domainStatusUnderVerification": { "message": "Под провером" @@ -10760,43 +10760,43 @@ } }, "domainClaimed": { - "message": "Domain claimed" + "message": "Домен захтеван" }, "organizationNameMaxLength": { "message": "Име организације не може прећи 50 знакова." }, "rotationCompletedTitle": { - "message": "Key rotation successful" + "message": "Успешна ротација кључа" }, "rotationCompletedDesc": { - "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + "message": "Ваша главна лозинка и кључеви за шифровање су ажурирани. Са ваших других уређаја сте одјављени." }, "trustUserEmergencyAccess": { - "message": "Trust and confirm user" + "message": "Поверуј и потврди корисника" }, "trustOrganization": { - "message": "Trust organization" + "message": "Поверуј организацији" }, "trust": { - "message": "Trust" + "message": "Повери" }, "doNotTrust": { - "message": "Do not trust" + "message": "Не повери" }, "organizationNotTrusted": { "message": "Организација није поверљива" }, "emergencyAccessTrustWarning": { - "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + "message": "За сигурност вашег налога, потврдите само ако сте добили хитни приступ овом кориснику и њиховим отискама одговарају оно што се приказује на њиховом налогу" }, "orgTrustWarning": { - "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + "message": "За сигурност вашег рачуна, наставите само ако сте члан ове организације, омогућили опоравак рачуна и отисак који се приказује испод одговара прстима организације." }, "orgTrustWarning1": { - "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." + "message": "Ова организација има политику за предузећа која ће вас уписати у опоравак налога. Упис ће омогућити администраторима организације да промене вашу лозинку. Наставите само ако препознајете ову организацију и ако се фраза отиска приказана испод поклапа са отиском организације." }, "trustUser": { - "message": "Trust user" + "message": "Повери кориснику" }, "sshKeyWrongPassword": { "message": "Лозинка коју сте унели није тачна." @@ -10861,7 +10861,7 @@ "description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon [Bitwarden Icon] from the toolbar.'" }, "resellerRenewalWarningMsg": { - "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "message": "Ваша претплата ће ускоро бити обновљена. Да бисте обезбедили непрекинуто пружање услуге, контактирајте $RESELLER$ ради потврде обновљења пре $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10904,7 +10904,7 @@ } }, "restartOrganizationSubscription": { - "message": "Organization subscription restarted" + "message": "Претплата организације је поново покренута" }, "restartSubscription": { "message": "Поново покрените претплату" @@ -10919,16 +10919,16 @@ } }, "accountDeprovisioningNotification": { - "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + "message": "Администратори сада имају могућност да бришу налоге чланова који припадају потврђеном домену." }, "deleteManagedUserWarningDesc": { - "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + "message": "Ова акција ће избрисати чланов налог заједно са свим ставкама у њиховом сефу. Ова акција замењује претходну опцију \"Уклони\"." }, "deleteManagedUserWarning": { "message": "Избриши је нова акција!" }, "seatsRemaining": { - "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "message": "Преостало вам је $REMAINING$ места од укупно $TOTAL$ додељених овој организацији. Контактирајте свог добављача да бисте управљали претплатом.", "placeholders": { "remaining": { "content": "$1", @@ -10947,13 +10947,13 @@ "message": "Select an organization to add to your Provider Portal." }, "noOrganizations": { - "message": "There are no organizations to list" + "message": "Нема организација за приказивање" }, "yourProviderSubscriptionCredit": { - "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + "message": "Ваша претплата код добављача ће добити повраћај средстава за преостало време претплате организације." }, "doYouWantToAddThisOrg": { - "message": "Do you want to add this organization to $PROVIDER$?", + "message": "Да ли желите да додате ову организацију у $PROVIDER$?", "placeholders": { "provider": { "content": "$1", @@ -10962,13 +10962,13 @@ } }, "addedExistingOrganization": { - "message": "Added existing organization" + "message": "Додата постојећа организација" }, "assignedExceedsAvailable": { - "message": "Assigned seats exceed available seats." + "message": "Додељена места премашују доступна." }, "userkeyRotationDisclaimerEmergencyAccessText": { - "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "message": "Фраза за идентификацију за $NUM_USERS$ контаката којима сте омогућили приступ у хитним случајевима.", "placeholders": { "num_users": { "content": "$1", @@ -10977,7 +10977,7 @@ } }, "userkeyRotationDisclaimerAccountRecoveryOrgsText": { - "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "message": "Идентификациона фраза за организацију $ORG_NAME$ за коју сте омогућили опоравак налога.", "placeholders": { "org_name": { "content": "$1", @@ -10986,25 +10986,25 @@ } }, "userkeyRotationDisclaimerDescription": { - "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + "message": "Ротирање ваших кључева за шифровање ће захтевати да поверујете кључеве организацијама које могу да опораве ваш налог, као и контактима којима сте омогућили приступ у хитним случајевима. Да бисте наставили, проверите следеће:" }, "userkeyRotationDisclaimerTitle": { - "message": "Untrusted encryption keys" + "message": "Неповерени кључеви за шифровање" }, "changeAtRiskPassword": { "message": "Променити ризичну лозинку" }, "changeAtRiskPasswordAndAddWebsite": { - "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." + "message": "Ова пријава је ризична и недостаје веб локација. Додајте веб страницу и промените лозинку за јачу сигурност." }, "missingWebsite": { - "message": "Missing website" + "message": "Недостаје веб страница" }, "removeUnlockWithPinPolicyTitle": { - "message": "Remove Unlock with PIN" + "message": "Уклони деблокирање ПИН-кодом" }, "removeUnlockWithPinPolicyDesc": { - "message": "Do not allow members to unlock their account with a PIN." + "message": "Не дозволи члановима да деблокирају свој налог помоћу ПИН-кода." }, "upgradeForFullEventsMessage": { "message": "Извештаји догађаја се не чувају за вашу организацију. Надоградите се у плану Teams или Enterprise да бисте добили потпуни приступ извештајима догађаја организације." @@ -11016,25 +11016,25 @@ "message": "Ови догађаји су само примери и не одражавају стварне догађаје у вашем Bitwarden отганизацији." }, "viewEvents": { - "message": "View Events" + "message": "Погледај Догађаје" }, "cannotCreateCollection": { "message": "Бесплатне организације могу имати до 2 колекције. Надоградите на плаћени план за додавање више колекција." }, "searchArchive": { - "message": "Search archive" + "message": "Претражи архиву" }, "archive": { - "message": "Archive" + "message": "Архива" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Нема ставка у архиви" }, "archivedItemsDescription": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, "businessUnit": { - "message": "Business Unit" + "message": "Пословна јединица" }, "businessUnits": { "message": "Business Units" @@ -11415,25 +11415,25 @@ "message": "Built-in authenticator" }, "breachMonitoring": { - "message": "Breach monitoring" + "message": "Праћење повreda безбедности" }, "andMoreFeatures": { - "message": "And more!" + "message": "И још више!" }, "secureFileStorage": { - "message": "Secure file storage" + "message": "Сигурно складиштење датотека" }, "familiesUnlimitedSharing": { - "message": "Unlimited sharing - choose who sees what" + "message": "Неограничено дељење – изаберите ко шта може да види" }, "familiesUnlimitedCollections": { "message": "Unlimited family collections" }, "familiesSharedStorage": { - "message": "Shared storage for important family info" + "message": "Заједничко складиштење важних породичних информација" }, "limitedUsersV2": { - "message": "Up to $COUNT$ members", + "message": "До $COUNT$ члана/ова", "placeholders": { "count": { "content": "$1", @@ -11442,7 +11442,7 @@ } }, "limitedCollectionsV2": { - "message": "Up to $COUNT$ collections", + "message": "До $COUNT$ колекција", "placeholders": { "count": { "content": "$1", @@ -11451,13 +11451,13 @@ } }, "alwaysFree": { - "message": "Always free" + "message": "Увек бесплатно" }, "twoSecretsIncluded": { - "message": "2 secrets" + "message": "2 тајне" }, "projectsIncludedV2": { - "message": "$COUNT$ project(s)", + "message": "$COUNT$ пројекта", "placeholders": { "count": { "content": "$1", @@ -11466,10 +11466,10 @@ } }, "secureItemSharing": { - "message": "Secure item sharing" + "message": "Сигурносно дељење ставка" }, "scimSupport": { - "message": "SCIM support" + "message": "SCIM подршка" }, "includedMachineAccountsV2": { "message": "$COUNT$ machine accounts", @@ -11484,18 +11484,18 @@ "message": "Enterprise security policies" }, "selfHostOption": { - "message": "Self-host option" + "message": "Могућност самосталног смештаја" }, "complimentaryFamiliesPlan": { "message": "Complimentary families plan for all users" }, "strengthenCybersecurity": { - "message": "Strengthen cybersecurity" + "message": "Ојачајте кибербезбедност" }, "boostProductivity": { - "message": "Boost productivity" + "message": "Повећајте продуктивност" }, "seamlessIntegration": { - "message": "Seamless integration" + "message": "Бешавна интеграција" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 6c5bb424a61..853e60c95bc 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -11418,7 +11418,7 @@ "message": "Breach monitoring" }, "andMoreFeatures": { - "message": "And more!" + "message": "Och mer!" }, "secureFileStorage": { "message": "Secure file storage" diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index c79e6460c33..f68cf42627d 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -7085,7 +7085,7 @@ "message": "Адреса е-пошти Catch-all" }, "catchallEmailDesc": { - "message": "Використовуйте свою скриньку вхідних Catch-All власного домену." + "message": "Використовуйте можливості Catch-All власного домену." }, "useThisEmail": { "message": "Використати цю е-пошту" diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 155a08a76be..9a87b3e2c6c 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -3,7 +3,7 @@ "message": "Tất cả các ứng dụng" }, "activity": { - "message": "Activity" + "message": "Hoạt động" }, "appLogoLabel": { "message": "Bitwarden logo" @@ -60,7 +60,7 @@ "message": "Tạo mục đăng nhập mới" }, "criticalApplicationsActivityDescription": { - "message": "Once you mark applications critical, they will display here." + "message": "Khi bạn đánh dấu các ứng dụng là quan trọng, chúng sẽ hiển thị tại đây." }, "criticalApplicationsWithCount": { "message": "Ứng dụng quan trọng ($COUNT$)", @@ -72,7 +72,7 @@ } }, "countOfCriticalApplications": { - "message": "$COUNT$ critical applications", + "message": "$COUNT$ ứng dụng quan trọng", "placeholders": { "count": { "content": "$1", @@ -135,10 +135,10 @@ "message": "Các thành viên có rủi ro" }, "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "message": "Thành viên có quyền chỉnh sửa đối với các mục có nguy cơ cho các ứng dụng quan trọng" }, "membersAtRisk": { - "message": "$COUNT$ members at risk", + "message": "$COUNT$ thành viên có nguy cơ", "placeholders": { "count": { "content": "$1", @@ -762,79 +762,79 @@ "message": "Xem mục" }, "newItemHeaderLogin": { - "message": "New Login", + "message": "Đăng nhập mới", "description": "Header for new login item type" }, "newItemHeaderCard": { - "message": "New Card", + "message": "Thẻ mới", "description": "Header for new card item type" }, "newItemHeaderIdentity": { - "message": "New Identity", + "message": "Danh tính mới", "description": "Header for new identity item type" }, "newItemHeaderNote": { - "message": "New Note", + "message": "Ghi chú mới", "description": "Header for new note item type" }, "newItemHeaderSshKey": { - "message": "New SSH key", + "message": "Khóa SSH mới", "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "New Text Send", + "message": "Send văn bản mới", "description": "Header for new text send" }, "newItemHeaderFileSend": { - "message": "New File Send", + "message": "Send tập tin mới", "description": "Header for new file send" }, "editItemHeaderLogin": { - "message": "Edit Login", + "message": "Chỉnh sửa đăng nhập", "description": "Header for edit login item type" }, "editItemHeaderCard": { - "message": "Edit Card", + "message": "Chỉnh sửa thẻ", "description": "Header for edit card item type" }, "editItemHeaderIdentity": { - "message": "Edit Identity", + "message": "Chỉnh sửa danh tính", "description": "Header for edit identity item type" }, "editItemHeaderNote": { - "message": "Edit Note", + "message": "Chỉnh sửa ghi chú", "description": "Header for edit note item type" }, "editItemHeaderSshKey": { - "message": "Edit SSH key", + "message": "Sửa khóa SSH", "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Edit Text Send", + "message": "Sửa Send văn bản", "description": "Header for edit text send" }, "editItemHeaderFileSend": { - "message": "Edit File Send", + "message": "Sửa Send tập tin", "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "Xem đăng nhập", "description": "Header for view login item type" }, "viewItemHeaderCard": { - "message": "View Card", + "message": "Xem thẻ", "description": "Header for view card item type" }, "viewItemHeaderIdentity": { - "message": "View Identity", + "message": "Xem danh tính", "description": "Header for view identity item type" }, "viewItemHeaderNote": { - "message": "View Note", + "message": "Xem ghi chú", "description": "Header for view note item type" }, "viewItemHeaderSshKey": { - "message": "View SSH key", + "message": "Xem khóa SSH", "description": "Header for view SSH key item type" }, "new": { @@ -2188,7 +2188,7 @@ "message": "Thay đổi ngôn ngữ của kho trên web." }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Hiển thị biểu tượng trang web và truy xuất các URL đổi mật khẩu" }, "default": { "message": "Mặc định" @@ -5558,10 +5558,10 @@ "message": "Do chính sách của doanh nghiệp, bạn không thể lưu trữ các mục vào kho cá nhân của mình. Hãy thay đổi tùy chọn Quyền sở hữu thành tổ chức và chọn từ các bộ sưu tập có sẵn." }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "Cài đặt Mặc định Tự động gõ phím trên Máy tính" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "Bật Tự động gõ phím trên Máy tính mặc định cho thành viên. Thành viên có thể tắt Tự động gõ phím thủ công trong ứng dụng máy tính để bàn.", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { @@ -6729,7 +6729,7 @@ "message": "SSO đã được bật" }, "emailMustLoginWithSso": { - "message": "$EMAIL$ must login with Single Sign-on", + "message": "$EMAIL$ phải đăng nhập bằng Đăng nhập một lần", "placeholders": { "email": { "content": "$1", @@ -7520,7 +7520,7 @@ "message": "Tắt" }, "connected": { - "message": "Connected" + "message": "Đã kết nối" }, "members": { "message": "Thành viên" @@ -9756,7 +9756,7 @@ "message": "Không thể lưu tích hợp. Vui lòng thử lại sau." }, "failedToDeleteIntegration": { - "message": "Failed to delete integration. Please try again later." + "message": "Không thể xóa tích hợp. Vui lòng thử lại sau." }, "deviceIdMissing": { "message": "Thiếu ID thiết bị" @@ -9783,7 +9783,7 @@ } }, "updateIntegrationButtonDesc": { - "message": "Update $INTEGRATION$", + "message": "Cập nhật $INTEGRATION$", "placeholders": { "integration": { "content": "$1", @@ -9855,7 +9855,7 @@ "message": "Bearer Token" }, "repositoryNameHint": { - "message": "Name of the repository to ingest into" + "message": "Tên kho lưu trữ để tiếp nhận vào" }, "index": { "message": "Mục lục" @@ -11022,16 +11022,16 @@ "message": "Các tổ chức miễn phí có thể có tối đa 2 bộ sưu tập. Nâng cấp lên gói trả phí để thêm nhiều bộ sưu tập hơn." }, "searchArchive": { - "message": "Search archive" + "message": "Tìm kiếm kho lưu trữ" }, "archive": { - "message": "Archive" + "message": "Lưu trữ" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Không có mục nào trong kho lưu trữ" }, "archivedItemsDescription": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, "businessUnit": { "message": "Bộ phận kinh doanh" @@ -11188,10 +11188,10 @@ "description": "Error message shown when trying to add credit to a trialing organization without a billing address." }, "aboutThisSetting": { - "message": "About this setting" + "message": "Về cài đặt này" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden sẽ sử dụng URI đăng nhập đã lưu để xác định biểu tượng hoặc URL đổi mật khẩu nào cần dùng nhằm cải thiện trải nghiệm của bạn. Không có thông tin nào được thu thập hay lưu lại khi bạn sử dụng dịch vụ này." }, "billingAddress": { "message": "Địa chỉ thanh toán" @@ -11367,34 +11367,34 @@ "message": "Xác minh ngay." }, "additionalStorageGB": { - "message": "Additional storage GB" + "message": "GB lưu trữ bổ sung" }, "additionalServiceAccountsV2": { - "message": "Additional machine accounts" + "message": "Tài khoản máy bổ sung" }, "secretsManagerSeats": { - "message": "Secrets Manager seats" + "message": "chỗ Trình quản lý Bí mật Bitwarden" }, "additionalStorage": { - "message": "Additional Storage" + "message": "Bộ nhớ Bổ sung" }, "expandPurchaseDetails": { - "message": "Expand purchase details" + "message": "Mở rộng chi tiết mua hàng" }, "collapsePurchaseDetails": { - "message": "Collapse purchase details" + "message": "Thu gọn chi tiết mua hàng" }, "familiesMembership": { - "message": "Families membership" + "message": "Gói Bitwarden Gia đình" }, "planDescPremium": { - "message": "Complete online security" + "message": "Bảo mật trực tuyến toàn diện" }, "planDescFamiliesV2": { - "message": "Premium security for your family" + "message": "Bảo mật cao cấp cho gia đình bạn" }, "planDescFreeV2": { - "message": "Share with $COUNT$ other user", + "message": "Chia sẻ với $COUNT$ người dùng khác", "placeholders": { "count": { "content": "$1", @@ -11403,37 +11403,37 @@ } }, "planDescEnterpriseV2": { - "message": "Advanced capabilities for any organization" + "message": "Khả năng nâng cao cho mọi tổ chức" }, "planNameCustom": { - "message": "Custom plan" + "message": "Gói tùy chỉnh" }, "planDescCustom": { - "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + "message": "Bitwarden mở rộng quy mô cùng với các doanh nghiệp thuộc mọi quy mô để bảo mật mật khẩu và thông tin nhạy cảm. Nếu bạn là một phần của doanh nghiệp lớn, hãy liên hệ với bộ phận bán hàng để yêu cầu báo giá." }, "builtInAuthenticator": { - "message": "Built-in authenticator" + "message": "Trình xác thực tích hợp" }, "breachMonitoring": { - "message": "Breach monitoring" + "message": "Giám sát vi phạm" }, "andMoreFeatures": { - "message": "And more!" + "message": "Và nhiều hơn nữa!" }, "secureFileStorage": { - "message": "Secure file storage" + "message": "Lưu trữ tệp an toàn" }, "familiesUnlimitedSharing": { - "message": "Unlimited sharing - choose who sees what" + "message": "Chia sẻ không giới hạn - chọn người xem nội dung nào" }, "familiesUnlimitedCollections": { - "message": "Unlimited family collections" + "message": "Bộ sưu tập gia đình không giới hạn" }, "familiesSharedStorage": { - "message": "Shared storage for important family info" + "message": "Bộ nhớ được chia sẻ cho thông tin gia đình quan trọng" }, "limitedUsersV2": { - "message": "Up to $COUNT$ members", + "message": "Tối đa $COUNT$ thành viên", "placeholders": { "count": { "content": "$1", @@ -11442,7 +11442,7 @@ } }, "limitedCollectionsV2": { - "message": "Up to $COUNT$ collections", + "message": "Tối đa $COUNT$ bộ sưu tập", "placeholders": { "count": { "content": "$1", @@ -11451,13 +11451,13 @@ } }, "alwaysFree": { - "message": "Always free" + "message": "Luôn miễn phí" }, "twoSecretsIncluded": { - "message": "2 secrets" + "message": "2 bí mật" }, "projectsIncludedV2": { - "message": "$COUNT$ project(s)", + "message": "$COUNT$ dự án", "placeholders": { "count": { "content": "$1", @@ -11466,13 +11466,13 @@ } }, "secureItemSharing": { - "message": "Secure item sharing" + "message": "Chia sẻ mục an toàn" }, "scimSupport": { - "message": "SCIM support" + "message": "Hỗ trợ SCIM" }, "includedMachineAccountsV2": { - "message": "$COUNT$ machine accounts", + "message": "$COUNT$ tài khoản máy", "placeholders": { "count": { "content": "$1", @@ -11481,21 +11481,21 @@ } }, "enterpriseSecurityPolicies": { - "message": "Enterprise security policies" + "message": "Chính sách bảo mật cấp doanh nghiệp" }, "selfHostOption": { - "message": "Self-host option" + "message": "Tùy chọn tự lưu trữ" }, "complimentaryFamiliesPlan": { - "message": "Complimentary families plan for all users" + "message": "Gói Bitwarden Gia đình miễn phí cho tất cả người dùng" }, "strengthenCybersecurity": { - "message": "Strengthen cybersecurity" + "message": "Tăng cường an ninh mạng" }, "boostProductivity": { - "message": "Boost productivity" + "message": "Tăng năng suất" }, "seamlessIntegration": { - "message": "Seamless integration" + "message": "Tích hợp liền mạch" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 423290706e0..38c585142df 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -5558,10 +5558,10 @@ "message": "由于某个企业策略,您不能将项目保存到您的个人密码库。请将所有权选项更改为组织,然后选择可用的集合。" }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "桌面端自动输入默认设置" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "默认为成员启用桌面端自动输入。成员可以在桌面客户端手动停用自动输入。", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { @@ -8486,7 +8486,7 @@ } }, "accessedProjectWithId": { - "message": "访问了 Id 为 $PROJECT_ID$ 的工程。", + "message": "访问了 ID 为 $PROJECT_ID$ 的工程。", "placeholders": { "project_id": { "content": "$1", @@ -8495,7 +8495,7 @@ } }, "nameUnavailableProjectDeleted": { - "message": "删除了 Id 为 $PROJECT_ID$ 的工程", + "message": "删除了 ID 为 $PROJECT_ID$ 的工程", "placeholders": { "project_id": { "content": "$1", @@ -8504,7 +8504,7 @@ } }, "nameUnavailableSecretDeleted": { - "message": "删除了 Id 为 $SECRET_ID$ 的机密", + "message": "删除了 ID 为 $SECRET_ID$ 的机密", "placeholders": { "secret_id": { "content": "$1", @@ -11364,7 +11364,7 @@ "message": "我们已向您的银行账户做了一笔小额转账,可能需要 1-2 个工作日到账。请在到账后验证您的银行账户。验证银行账户失败将会错过支付,您的订阅将暂停。" }, "verifyNow": { - "message": " 立即验证。" + "message": "立即验证。" }, "additionalStorageGB": { "message": "附加存储 GB" From adbf80dd39c841e710b6f64cf69872cd388cf01c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:31:31 +0200 Subject: [PATCH 20/55] [deps] Platform: Update Rust crate security-framework to v3.5.0 (#16628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García --- apps/desktop/desktop_native/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index b8f582f43de..0a637b12de9 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -49,7 +49,7 @@ rsa = "=0.9.6" russh-cryptovec = "=0.7.3" scopeguard = "=1.2.0" secmem-proc = "=0.3.7" -security-framework = "=3.4.0" +security-framework = "=3.5.0" security-framework-sys = "=2.15.0" serde = "=1.0.209" serde_json = "=1.0.127" From 90fb57817a110461cbdb46f09e4d51e46f83b886 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 29 Sep 2025 08:27:21 -0500 Subject: [PATCH 21/55] [PM-25931] Integrations - can save only if owner (#16570) --- apps/web/src/locales/en/messages.json | 3 + .../hec-organization-integration-service.ts | 164 +++++++++++------- .../organization-integration-api.service.ts | 20 ++- .../integration-card.component.spec.ts | 76 +++++++- .../integration-card.component.ts | 26 ++- .../integrations.component.ts | 44 +++-- 6 files changed, 232 insertions(+), 101 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 1646235d8cb..b4ad8f2b2b2 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9759,6 +9759,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts index ad3d6764713..6c6a086e0f5 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts @@ -1,5 +1,6 @@ import { BehaviorSubject, firstValueFrom, map, Subject, switchMap, takeUntil, zip } from "rxjs"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { OrganizationId, OrganizationIntegrationId, @@ -20,6 +21,11 @@ import { OrganizationIntegrationType } from "../models/organization-integration- import { OrganizationIntegrationApiService } from "./organization-integration-api.service"; import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service"; +export type HecModificationFailureReason = { + mustBeOwner: boolean; + success: boolean; +}; + export class HecOrganizationIntegrationService { private organizationId$ = new BehaviorSubject(null); private _integrations$ = new BehaviorSubject([]); @@ -34,7 +40,7 @@ export class HecOrganizationIntegrationService { const data$ = await this.setIntegrations(orgId); return await firstValueFrom(data$); } else { - return this._integrations$.getValue(); + return [] as OrganizationIntegration[]; } }), takeUntil(this.destroy$), @@ -56,6 +62,10 @@ export class HecOrganizationIntegrationService { * @param orgId */ setOrganizationIntegrations(orgId: OrganizationId) { + if (orgId == this.organizationId$.getValue()) { + return; + } + this._integrations$.next([]); this.organizationId$.next(orgId); } @@ -73,31 +83,39 @@ export class HecOrganizationIntegrationService { url: string, bearerToken: string, index: string, - ) { + ): Promise { if (organizationId != this.organizationId$.getValue()) { throw new Error("Organization ID mismatch"); } - const hecConfig = new HecConfiguration(url, bearerToken, service); - const newIntegrationResponse = await this.integrationApiService.createOrganizationIntegration( - organizationId, - new OrganizationIntegrationRequest(OrganizationIntegrationType.Hec, hecConfig.toString()), - ); - - const newTemplate = new HecTemplate(index, service); - const newIntegrationConfigResponse = - await this.integrationConfigurationApiService.createOrganizationIntegrationConfiguration( + try { + const hecConfig = new HecConfiguration(url, bearerToken, service); + const newIntegrationResponse = await this.integrationApiService.createOrganizationIntegration( organizationId, - newIntegrationResponse.id, - new OrganizationIntegrationConfigurationRequest(null, null, null, newTemplate.toString()), + new OrganizationIntegrationRequest(OrganizationIntegrationType.Hec, hecConfig.toString()), ); - const newIntegration = this.mapResponsesToOrganizationIntegration( - newIntegrationResponse, - newIntegrationConfigResponse, - ); - if (newIntegration !== null) { - this._integrations$.next([...this._integrations$.getValue(), newIntegration]); + const newTemplate = new HecTemplate(index, service); + const newIntegrationConfigResponse = + await this.integrationConfigurationApiService.createOrganizationIntegrationConfiguration( + organizationId, + newIntegrationResponse.id, + new OrganizationIntegrationConfigurationRequest(null, null, null, newTemplate.toString()), + ); + + const newIntegration = this.mapResponsesToOrganizationIntegration( + newIntegrationResponse, + newIntegrationConfigResponse, + ); + if (newIntegration !== null) { + this._integrations$.next([...this._integrations$.getValue(), newIntegration]); + } + return { mustBeOwner: false, success: true }; + } catch (error) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + return { mustBeOwner: true, success: false }; + } + throw error; } } @@ -119,40 +137,48 @@ export class HecOrganizationIntegrationService { url: string, bearerToken: string, index: string, - ) { + ): Promise { if (organizationId != this.organizationId$.getValue()) { throw new Error("Organization ID mismatch"); } - const hecConfig = new HecConfiguration(url, bearerToken, service); - const updatedIntegrationResponse = - await this.integrationApiService.updateOrganizationIntegration( - organizationId, - OrganizationIntegrationId, - new OrganizationIntegrationRequest(OrganizationIntegrationType.Hec, hecConfig.toString()), + try { + const hecConfig = new HecConfiguration(url, bearerToken, service); + const updatedIntegrationResponse = + await this.integrationApiService.updateOrganizationIntegration( + organizationId, + OrganizationIntegrationId, + new OrganizationIntegrationRequest(OrganizationIntegrationType.Hec, hecConfig.toString()), + ); + + const updatedTemplate = new HecTemplate(index, service); + const updatedIntegrationConfigResponse = + await this.integrationConfigurationApiService.updateOrganizationIntegrationConfiguration( + organizationId, + OrganizationIntegrationId, + OrganizationIntegrationConfigurationId, + new OrganizationIntegrationConfigurationRequest( + null, + null, + null, + updatedTemplate.toString(), + ), + ); + + const updatedIntegration = this.mapResponsesToOrganizationIntegration( + updatedIntegrationResponse, + updatedIntegrationConfigResponse, ); - const updatedTemplate = new HecTemplate(index, service); - const updatedIntegrationConfigResponse = - await this.integrationConfigurationApiService.updateOrganizationIntegrationConfiguration( - organizationId, - OrganizationIntegrationId, - OrganizationIntegrationConfigurationId, - new OrganizationIntegrationConfigurationRequest( - null, - null, - null, - updatedTemplate.toString(), - ), - ); - - const updatedIntegration = this.mapResponsesToOrganizationIntegration( - updatedIntegrationResponse, - updatedIntegrationConfigResponse, - ); - - if (updatedIntegration !== null) { - this._integrations$.next([...this._integrations$.getValue(), updatedIntegration]); + if (updatedIntegration !== null) { + this._integrations$.next([...this._integrations$.getValue(), updatedIntegration]); + } + return { mustBeOwner: false, success: true }; + } catch (error) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + return { mustBeOwner: true, success: false }; + } + throw error; } } @@ -160,28 +186,38 @@ export class HecOrganizationIntegrationService { organizationId: OrganizationId, OrganizationIntegrationId: OrganizationIntegrationId, OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId, - ) { + ): Promise { if (organizationId != this.organizationId$.getValue()) { throw new Error("Organization ID mismatch"); } - // delete the configuration first due to foreign key constraint - await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration( - organizationId, - OrganizationIntegrationId, - OrganizationIntegrationConfigurationId, - ); - // delete the integration - await this.integrationApiService.deleteOrganizationIntegration( - organizationId, - OrganizationIntegrationId, - ); + try { + // delete the configuration first due to foreign key constraint + await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration( + organizationId, + OrganizationIntegrationId, + OrganizationIntegrationConfigurationId, + ); - // update the local observable - const updatedIntegrations = this._integrations$ - .getValue() - .filter((i) => i.id !== OrganizationIntegrationId); - this._integrations$.next(updatedIntegrations); + // delete the integration + await this.integrationApiService.deleteOrganizationIntegration( + organizationId, + OrganizationIntegrationId, + ); + + // update the local observable + const updatedIntegrations = this._integrations$ + .getValue() + .filter((i) => i.id !== OrganizationIntegrationId); + this._integrations$.next(updatedIntegrations); + + return { mustBeOwner: false, success: true }; + } catch (error) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + return { mustBeOwner: true, success: false }; + } + throw error; + } } /** diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-api.service.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-api.service.ts index 17dac165baa..b6d2540d9d7 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-api.service.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-api.service.ts @@ -10,14 +10,18 @@ export class OrganizationIntegrationApiService { async getOrganizationIntegrations( orgId: OrganizationId, ): Promise { - const response = await this.apiService.send( - "GET", - `/organizations/${orgId}/integrations`, - null, - true, - true, - ); - return response; + try { + const response = await this.apiService.send( + "GET", + `/organizations/${orgId}/integrations`, + null, + true, + true, + ); + return response; + } catch { + return []; + } } async createOrganizationIntegration( diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts index 0facf282ba3..74c39613502 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts @@ -6,6 +6,7 @@ import { BehaviorSubject, of } from "rxjs"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; @@ -314,7 +315,7 @@ describe("IntegrationCardComponent", () => { jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(false); - mockIntegrationService.saveHec.mockResolvedValue(undefined); + mockIntegrationService.saveHec.mockResolvedValue({ mustBeOwner: false, success: true }); await component.setupConnection(); @@ -340,7 +341,7 @@ describe("IntegrationCardComponent", () => { }), }); - mockIntegrationService.deleteHec.mockResolvedValue(undefined); + mockIntegrationService.deleteHec.mockResolvedValue({ mustBeOwner: false, success: true }); await component.setupConnection(); @@ -368,7 +369,7 @@ describe("IntegrationCardComponent", () => { }), }); - mockIntegrationService.deleteHec.mockResolvedValue(undefined); + mockIntegrationService.deleteHec.mockResolvedValue({ mustBeOwner: false, success: true }); await component.setupConnection(); @@ -407,6 +408,52 @@ describe("IntegrationCardComponent", () => { }); }); + it("should show mustBeOwner toast on error while inserting data", async () => { + (openHecConnectDialog as jest.Mock).mockReturnValue({ + closed: of({ + success: HecConnectDialogResultStatus.Edited, + url: "test-url", + bearerToken: "token", + index: "index", + }), + }); + + jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true); + mockIntegrationService.updateHec.mockRejectedValue(new ErrorResponse("Not Found", 404)); + + await component.setupConnection(); + + expect(mockIntegrationService.updateHec).toHaveBeenCalled(); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "", + message: mockI18nService.t("mustBeOrgOwnerToPerformAction"), + }); + }); + + it("should show mustBeOwner toast on error while updating data", async () => { + (openHecConnectDialog as jest.Mock).mockReturnValue({ + closed: of({ + success: HecConnectDialogResultStatus.Edited, + url: "test-url", + bearerToken: "token", + index: "index", + }), + }); + + jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true); + mockIntegrationService.updateHec.mockRejectedValue(new ErrorResponse("Not Found", 404)); + + await component.setupConnection(); + + expect(mockIntegrationService.updateHec).toHaveBeenCalled(); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "", + message: mockI18nService.t("mustBeOrgOwnerToPerformAction"), + }); + }); + it("should show toast on error while deleting", async () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ @@ -429,5 +476,28 @@ describe("IntegrationCardComponent", () => { message: mockI18nService.t("failedToDeleteIntegration"), }); }); + + it("should show mustbeOwner toast on 404 while deleting", async () => { + (openHecConnectDialog as jest.Mock).mockReturnValue({ + closed: of({ + success: HecConnectDialogResultStatus.Delete, + url: "test-url", + bearerToken: "token", + index: "index", + }), + }); + + jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true); + mockIntegrationService.deleteHec.mockRejectedValue(new ErrorResponse("Not Found", 404)); + + await component.setupConnection(); + + expect(mockIntegrationService.deleteHec).toHaveBeenCalled(); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "", + message: mockI18nService.t("mustBeOrgOwnerToPerformAction"), + }); + }); }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts index 99e8d950d81..091de63d7a1 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts @@ -171,6 +171,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { } async saveHec(result: HecConnectDialogResult) { + let saveResponse = { mustBeOwner: false, success: false }; if (this.isUpdateAvailable) { // retrieve org integration and configuration ids const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; @@ -182,7 +183,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { } // update existing integration and configuration - await this.hecOrganizationIntegrationService.updateHec( + saveResponse = await this.hecOrganizationIntegrationService.updateHec( this.organizationId, orgIntegrationId, orgIntegrationConfigurationId, @@ -193,7 +194,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { ); } else { // create new integration and configuration - await this.hecOrganizationIntegrationService.saveHec( + saveResponse = await this.hecOrganizationIntegrationService.saveHec( this.organizationId, this.integrationSettings.name as OrganizationIntegrationServiceType, result.url, @@ -201,6 +202,12 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { result.index, ); } + + if (saveResponse.mustBeOwner) { + this.showMustBeOwnerToast(); + return; + } + this.toastService.showToast({ variant: "success", title: "", @@ -217,16 +224,29 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { throw Error("Organization Integration ID or Configuration ID is missing"); } - await this.hecOrganizationIntegrationService.deleteHec( + const response = await this.hecOrganizationIntegrationService.deleteHec( this.organizationId, orgIntegrationId, orgIntegrationConfigurationId, ); + if (response.mustBeOwner) { + this.showMustBeOwnerToast(); + return; + } + this.toastService.showToast({ variant: "success", title: "", message: this.i18nService.t("success"), }); } + + private showMustBeOwnerToast() { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("mustBeOrgOwnerToPerformAction"), + }); + } } diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts index e3af5e273ea..c249bf42282 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts @@ -5,16 +5,14 @@ import { firstValueFrom, Observable, Subject, switchMap, takeUntil, takeWhile } import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service"; -import { - getOrganizationById, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { IntegrationType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { getById } from "@bitwarden/common/platform/misc"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; @@ -218,7 +216,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { this.organization$ = this.route.params.pipe( switchMap((params) => this.organizationService.organizations$(userId).pipe( - getOrganizationById(params.organizationId), + getById(params.organizationId), // Filter out undefined values takeWhile((org: Organization | undefined) => !!org), ), @@ -229,6 +227,24 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { this.organization$.pipe(takeUntil(this.destroy$)).subscribe((org) => { this.hecOrganizationIntegrationService.setOrganizationIntegrations(org.id); }); + + // For all existing event based configurations loop through and assign the + // organizationIntegration for the correct services. + this.hecOrganizationIntegrationService.integrations$ + .pipe(takeUntil(this.destroy$)) + .subscribe((integrations) => { + // reset all integrations to null first - in case one was deleted + this.integrationsList.forEach((i) => { + i.organizationIntegration = null; + }); + + integrations.map((integration) => { + const item = this.integrationsList.find((i) => i.name === integration.serviceType); + if (item) { + item.organizationIntegration = integration; + } + }); + }); } constructor( @@ -258,24 +274,6 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { this.integrationsList.push(crowdstrikeIntegration); } - - // For all existing event based configurations loop through and assign the - // organizationIntegration for the correct services. - this.hecOrganizationIntegrationService.integrations$ - .pipe(takeUntil(this.destroy$)) - .subscribe((integrations) => { - // reset all integrations to null first - in case one was deleted - this.integrationsList.forEach((i) => { - i.organizationIntegration = null; - }); - - integrations.map((integration) => { - const item = this.integrationsList.find((i) => i.name === integration.serviceType); - if (item) { - item.organizationIntegration = integration; - } - }); - }); } ngOnDestroy(): void { this.destroy$.next(); From 018b4d5eb4c93ef25b977f656f042000a5200cd8 Mon Sep 17 00:00:00 2001 From: Mark Youssef <141061617+mark-youssef-bitwarden@users.noreply.github.com> Date: Mon, 29 Sep 2025 07:19:52 -0700 Subject: [PATCH 22/55] [CL-609] Close side nav when breakpoint changes (#15062) * Close side nav when breakpoint changes * Leverage side-nave listener instead * Remove effect inside pipe * Reuse isSmallScreen --- libs/components/src/navigation/side-nav.service.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/libs/components/src/navigation/side-nav.service.ts b/libs/components/src/navigation/side-nav.service.ts index 87691244ca4..5a67f2c965b 100644 --- a/libs/components/src/navigation/side-nav.service.ts +++ b/libs/components/src/navigation/side-nav.service.ts @@ -1,4 +1,5 @@ import { Injectable } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { BehaviorSubject, Observable, combineLatest, fromEvent, map, startWith } from "rxjs"; @Injectable({ @@ -8,10 +9,20 @@ export class SideNavService { private _open$ = new BehaviorSubject(!window.matchMedia("(max-width: 768px)").matches); open$ = this._open$.asObservable(); - isOverlay$ = combineLatest([this.open$, media("(max-width: 768px)")]).pipe( + private isSmallScreen$ = media("(max-width: 768px)"); + + isOverlay$ = combineLatest([this.open$, this.isSmallScreen$]).pipe( map(([open, isSmallScreen]) => open && isSmallScreen), ); + constructor() { + this.isSmallScreen$.pipe(takeUntilDestroyed()).subscribe((isSmallScreen) => { + if (isSmallScreen) { + this.setClose(); + } + }); + } + get open() { return this._open$.getValue(); } From fc53eae4c5df8d0d4b696ea2fa8b09f39e13d5d7 Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Mon, 29 Sep 2025 10:20:15 -0400 Subject: [PATCH 23/55] [PM-22758] Configurable Keyboard Shortcut for Autotype (#16613) * [PM-22785] Initial push with configuration and ipc changes for the configurable autotype keyboard shortcut * [PM-22785] Add messy code with working configurable hotkey * [PM-22785] Add more messy rust code * [PM-22785] Add temp changes with configurable hotkey ui * Add shortcut display to settings * [PM-22785] Logic updates. Ran npm run prettier and lint:fix. * [PM-22785] Add back disableAutotype with refactors. * [PM-22785] Clean up Rust code * [PM-22785] Clean up Rust code v2 * [PM-22785] Add unicode bounds in Rust code * [PM-22785] Update rust code comments * [PM-22785] Add unicode_value byte length check post-encoding * [PM-22785] Extract encoding to a separate function * Various fixes for the autotype setting label * Misc component fixes * Disallow nunmbers and allow Win key * Themify edit shortcut * Change display of Super to Win * Create autotype format method * Autotpe modal cleanup * [PM-22785] Some cleanup * Add unit tests and adjust error handling * [PM-22785] Fix build issues on Mac and Linux * [PM-22785] Linting fix * Remove unused message * [PM-22785] Linting fix * [PM-22785] More linting fix * [PM-22785] Address initial PR comments * [PM-22785] Comment change * [PM-22785] If statement change * [PM-22785] Update with fixes from PR comments * [PM-22785] Update with fixes from PR comments version ? * add unit tests for get_alphabetic_hot_key() * Fix tests * Add missing mock to tests * [PM-22785] Update with small fixes via PR comments --------- Co-authored-by: Robyn MacCallum Co-authored-by: neuronull <9162534+neuronull@users.noreply.github.com> --- .../desktop_native/autotype/src/lib.rs | 4 +- .../desktop_native/autotype/src/linux.rs | 5 +- .../desktop_native/autotype/src/macos.rs | 9 +- .../desktop_native/autotype/src/windows.rs | 96 +++++- apps/desktop/desktop_native/napi/index.d.ts | 2 +- apps/desktop/desktop_native/napi/src/lib.rs | 7 +- .../src/app/accounts/settings.component.html | 13 +- .../app/accounts/settings.component.spec.ts | 1 + .../src/app/accounts/settings.component.ts | 33 ++ .../autotype-shortcut.component.html | 33 ++ .../autotype-shortcut.component.spec.ts | 281 ++++++++++++++++++ .../components/autotype-shortcut.component.ts | 139 +++++++++ .../main/main-desktop-autotype.service.ts | 68 +++-- .../models/main-autotype-keyboard-shortcut.ts | 98 ++++++ apps/desktop/src/autofill/preload.ts | 4 +- .../services/desktop-autotype.service.ts | 36 ++- apps/desktop/src/locales/en/messages.json | 16 +- apps/desktop/src/scss/misc.scss | 7 + 18 files changed, 802 insertions(+), 50 deletions(-) create mode 100644 apps/desktop/src/autofill/components/autotype-shortcut.component.html create mode 100644 apps/desktop/src/autofill/components/autotype-shortcut.component.spec.ts create mode 100644 apps/desktop/src/autofill/components/autotype-shortcut.component.ts create mode 100644 apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts diff --git a/apps/desktop/desktop_native/autotype/src/lib.rs b/apps/desktop/desktop_native/autotype/src/lib.rs index 6d7b9f9db85..f1aab2ba164 100644 --- a/apps/desktop/desktop_native/autotype/src/lib.rs +++ b/apps/desktop/desktop_native/autotype/src/lib.rs @@ -17,6 +17,6 @@ pub fn get_foreground_window_title() -> std::result::Result { /// /// TODO: The error handling will be improved in a future PR: PM-23615 #[allow(clippy::result_unit_err)] -pub fn type_input(input: Vec) -> std::result::Result<(), ()> { - windowing::type_input(input) +pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> std::result::Result<(), ()> { + windowing::type_input(input, keyboard_shortcut) } diff --git a/apps/desktop/desktop_native/autotype/src/linux.rs b/apps/desktop/desktop_native/autotype/src/linux.rs index d53d7af0bd9..148b1aab6eb 100644 --- a/apps/desktop/desktop_native/autotype/src/linux.rs +++ b/apps/desktop/desktop_native/autotype/src/linux.rs @@ -2,6 +2,9 @@ pub fn get_foreground_window_title() -> std::result::Result { todo!("Bitwarden does not yet support Linux autotype"); } -pub fn type_input(_input: Vec) -> std::result::Result<(), ()> { +pub fn type_input( + _input: Vec, + _keyboard_shortcut: Vec, +) -> std::result::Result<(), ()> { todo!("Bitwarden does not yet support Linux autotype"); } diff --git a/apps/desktop/desktop_native/autotype/src/macos.rs b/apps/desktop/desktop_native/autotype/src/macos.rs index 7ab9f5441b7..5542e7a3a6b 100644 --- a/apps/desktop/desktop_native/autotype/src/macos.rs +++ b/apps/desktop/desktop_native/autotype/src/macos.rs @@ -1,7 +1,10 @@ pub fn get_foreground_window_title() -> std::result::Result { - todo!("Bitwarden does not yet support Mac OS autotype"); + todo!("Bitwarden does not yet support macOS autotype"); } -pub fn type_input(_input: Vec) -> std::result::Result<(), ()> { - todo!("Bitwarden does not yet support Mac OS autotype"); +pub fn type_input( + _input: Vec, + _keyboard_shortcut: Vec, +) -> std::result::Result<(), ()> { + todo!("Bitwarden does not yet support macOS autotype"); } diff --git a/apps/desktop/desktop_native/autotype/src/windows.rs b/apps/desktop/desktop_native/autotype/src/windows.rs index f1f9bee7f60..1d39d3f7ae5 100644 --- a/apps/desktop/desktop_native/autotype/src/windows.rs +++ b/apps/desktop/desktop_native/autotype/src/windows.rs @@ -25,25 +25,29 @@ pub fn get_foreground_window_title() -> std::result::Result { /// Attempts to type the input text wherever the user's cursor is. /// -/// `input` must be an array of utf-16 encoded characters to insert. +/// `input` must be a vector of utf-16 encoded characters to insert. +/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super, Shift, letters a - Z /// /// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput -pub fn type_input(input: Vec) -> Result<(), ()> { - const TAB_KEY: u16 = 9; +pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<(), ()> { + const TAB_KEY: u8 = 9; + let mut keyboard_inputs: Vec = Vec::new(); - // Release hotkeys - keyboard_inputs.push(build_virtual_key_input(InputKeyPress::Up, 0x11)); // ctrl - keyboard_inputs.push(build_virtual_key_input(InputKeyPress::Up, 0x10)); // shift - keyboard_inputs.push(build_unicode_input(InputKeyPress::Up, 42)); // b + // Add key "up" inputs for the shortcut + for key in keyboard_shortcut { + keyboard_inputs.push(convert_shortcut_key_to_up_input(key)?); + } + // Add key "down" and "up" inputs for the input + // (currently in this form: {username}/t{password}) for i in input { - let next_down_input = if i == TAB_KEY { + let next_down_input = if i == TAB_KEY.into() { build_virtual_key_input(InputKeyPress::Down, i as u8) } else { build_unicode_input(InputKeyPress::Down, i) }; - let next_up_input = if i == TAB_KEY { + let next_up_input = if i == TAB_KEY.into() { build_virtual_key_input(InputKeyPress::Up, i as u8) } else { build_unicode_input(InputKeyPress::Up, i) @@ -56,6 +60,51 @@ pub fn type_input(input: Vec) -> Result<(), ()> { send_input(keyboard_inputs) } +/// Converts a valid shortcut key to an "up" keyboard input. +/// +/// `input` must be a valid shortcut key: Control, Alt, Super, Shift, letters [a-z][A-Z] +fn convert_shortcut_key_to_up_input(key: String) -> Result { + const SHIFT_KEY: u8 = 0x10; + const SHIFT_KEY_STR: &str = "Shift"; + const CONTROL_KEY: u8 = 0x11; + const CONTROL_KEY_STR: &str = "Control"; + const ALT_KEY: u8 = 0x12; + const ALT_KEY_STR: &str = "Alt"; + const LEFT_WINDOWS_KEY: u8 = 0x5B; + const LEFT_WINDOWS_KEY_STR: &str = "Super"; + + Ok(match key.as_str() { + SHIFT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, SHIFT_KEY), + CONTROL_KEY_STR => build_virtual_key_input(InputKeyPress::Up, CONTROL_KEY), + ALT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, ALT_KEY), + LEFT_WINDOWS_KEY_STR => build_virtual_key_input(InputKeyPress::Up, LEFT_WINDOWS_KEY), + _ => build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?), + }) +} + +/// Given a letter that is a String, get the utf16 encoded +/// decimal version of the letter as long as it meets the +/// [a-z][A-Z] restriction. +/// +/// Because we only accept [a-z][A-Z], the decimal u16 +/// cast of the letter is safe because the unicode code point +/// of these characters fits in a u16. +fn get_alphabetic_hotkey(letter: String) -> Result { + if letter.len() != 1 { + return Err(()); + } + + let c = letter.chars().next().expect("letter is size 1"); + + // is_ascii_alphabetic() checks for: + // U+0041 `A` ..= U+005A `Z`, or U+0061 `a` ..= U+007A `z` + if !c.is_ascii_alphabetic() { + return Err(()); + } + + Ok(c as u16) +} + /// Gets the foreground window handle. /// /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow @@ -198,3 +247,32 @@ fn send_input(inputs: Vec) -> Result<(), ()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_alphabetic_hot_key_happy() { + for c in ('a'..='z').chain('A'..='Z') { + let letter = c.to_string(); + println!("{}", letter); + let converted = get_alphabetic_hotkey(letter).unwrap(); + assert_eq!(converted, c as u16); + } + } + + #[test] + #[should_panic = ""] + fn get_alphabetic_hot_key_fail_not_single_char() { + let letter = String::from("foo"); + get_alphabetic_hotkey(letter).unwrap(); + } + + #[test] + #[should_panic = ""] + fn get_alphabetic_hot_key_fail_not_alphabetic() { + let letter = String::from("🚀"); + get_alphabetic_hotkey(letter).unwrap(); + } +} diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 2212c03f4f8..030bf4c964d 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -234,5 +234,5 @@ export declare namespace chromium_importer { } export declare namespace autotype { export function getForegroundWindowTitle(): string - export function typeInput(input: Array): void + export function typeInput(input: Array, keyboardShortcut: Array): void } diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 4731166852b..327c7c1c8e5 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -1044,8 +1044,11 @@ pub mod autotype { } #[napi] - pub fn type_input(input: Vec) -> napi::Result<(), napi::Status> { - autotype::type_input(input).map_err(|_| { + pub fn type_input( + input: Vec, + keyboard_shortcut: Vec, + ) -> napi::Result<(), napi::Status> { + autotype::type_input(input, keyboard_shortcut).map_err(|_| { napi::Error::from_reason("Autotype Error: failed to type input".to_string()) }) } diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 18f7f67abc2..a0380a8b5ce 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -340,7 +340,10 @@ (change)="saveEnableAutotype()" />
- {{ "enableAutotypeTransitionKey" | i18n }} + {{ "enableAutotypeShortcutPreview" | i18n }} +
+ {{ form.value.autotypeShortcut }} +
@@ -348,7 +351,13 @@ {{ "important" | i18n }} {{ "enableAutotypeDescriptionTransitionKey" | i18n }} - {{ "editShortcut" | i18n }} + {{ "editShortcut" | i18n }} +
diff --git a/apps/desktop/src/app/accounts/settings.component.spec.ts b/apps/desktop/src/app/accounts/settings.component.spec.ts index 082801cba0a..a791fd7b9a4 100644 --- a/apps/desktop/src/app/accounts/settings.component.spec.ts +++ b/apps/desktop/src/app/accounts/settings.component.spec.ts @@ -183,6 +183,7 @@ describe("SettingsComponent", () => { policyService.policiesByType$.mockReturnValue(of([null])); desktopAutotypeService.resolvedAutotypeEnabled$ = of(false); desktopAutotypeService.autotypeEnabledUserSetting$ = of(false); + desktopAutotypeService.autotypeKeyboardShortcut$ = of(["Control", "Shift", "B"]); billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false)); configService.getFeatureFlag$.mockReturnValue(of(true)); }); diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 0ec77419d02..030027913bc 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -58,6 +58,7 @@ import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/ import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault"; import { SetPinComponent } from "../../auth/components/set-pin.component"; +import { AutotypeShortcutComponent } from "../../autofill/components/autotype-shortcut.component"; import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service"; @@ -111,6 +112,7 @@ export class SettingsComponent implements OnInit, OnDestroy { requireEnableTray = false; showDuckDuckGoIntegrationOption = false; showEnableAutotype = false; + autotypeShortcut: string; showOpenAtLoginOption = false; isWindows: boolean; isLinux: boolean; @@ -173,6 +175,7 @@ export class SettingsComponent implements OnInit, OnDestroy { value: false, disabled: true, }), + autotypeShortcut: [null as string | null], theme: [null as Theme | null], locale: [null as string | null], }); @@ -397,6 +400,9 @@ export class SettingsComponent implements OnInit, OnDestroy { ), allowScreenshots: !(await firstValueFrom(this.desktopSettingsService.preventScreenshots$)), enableAutotype: await firstValueFrom(this.desktopAutotypeService.autotypeEnabledUserSetting$), + autotypeShortcut: this.getFormattedAutotypeShortcutText( + (await firstValueFrom(this.desktopAutotypeService.autotypeKeyboardShortcut$)) ?? [], + ), theme: await firstValueFrom(this.themeStateService.selectedTheme$), locale: await firstValueFrom(this.i18nService.userSetLocale$), }; @@ -897,6 +903,29 @@ export class SettingsComponent implements OnInit, OnDestroy { async saveEnableAutotype() { await this.desktopAutotypeService.setAutotypeEnabledState(this.form.value.enableAutotype); + const currentShortcut = await firstValueFrom( + this.desktopAutotypeService.autotypeKeyboardShortcut$, + ); + if (currentShortcut) { + this.form.controls.autotypeShortcut.setValue( + this.getFormattedAutotypeShortcutText(currentShortcut), + ); + } + } + + async saveAutotypeShortcut() { + const dialogRef = AutotypeShortcutComponent.open(this.dialogService); + + const newShortcutArray = await firstValueFrom(dialogRef.closed); + + if (!newShortcutArray) { + return; + } + + this.form.controls.autotypeShortcut.setValue( + this.getFormattedAutotypeShortcutText(newShortcutArray), + ); + await this.desktopAutotypeService.setAutotypeKeyboardShortcutState(newShortcutArray); } private async generateVaultTimeoutOptions(): Promise { @@ -944,4 +973,8 @@ export class SettingsComponent implements OnInit, OnDestroy { throw new Error("Unsupported platform"); } } + + getFormattedAutotypeShortcutText(shortcut: string[]) { + return shortcut ? shortcut.join("+").replace("Super", "Win") : null; + } } diff --git a/apps/desktop/src/autofill/components/autotype-shortcut.component.html b/apps/desktop/src/autofill/components/autotype-shortcut.component.html new file mode 100644 index 00000000000..774c299e0b6 --- /dev/null +++ b/apps/desktop/src/autofill/components/autotype-shortcut.component.html @@ -0,0 +1,33 @@ +
+ +
+ {{ "typeShortcut" | i18n }} +
+
+

+ {{ "editAutotypeShortcutDescription" | i18n }} +

+ + {{ "typeShortcut" | i18n }} + + +
+ + + + +
+
diff --git a/apps/desktop/src/autofill/components/autotype-shortcut.component.spec.ts b/apps/desktop/src/autofill/components/autotype-shortcut.component.spec.ts new file mode 100644 index 00000000000..90aa493c596 --- /dev/null +++ b/apps/desktop/src/autofill/components/autotype-shortcut.component.spec.ts @@ -0,0 +1,281 @@ +import { AbstractControl, FormBuilder, ValidationErrors } from "@angular/forms"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { AutotypeShortcutComponent } from "./autotype-shortcut.component"; + +describe("AutotypeShortcutComponent", () => { + let component: AutotypeShortcutComponent; + let validator: (control: AbstractControl) => ValidationErrors | null; + let formBuilder: MockProxy; + let i18nService: MockProxy; + + beforeEach(() => { + formBuilder = mock(); + i18nService = mock(); + i18nService.t.mockReturnValue("Invalid shortcut"); + component = new AutotypeShortcutComponent(null as any, formBuilder, i18nService); + validator = component["shortcutCombinationValidator"](); + }); + + describe("shortcutCombinationValidator", () => { + const createControl = (value: string | null): AbstractControl => + ({ + value, + }) as AbstractControl; + + describe("valid shortcuts", () => { + it("should accept single modifier with letter", () => { + const validShortcuts = [ + "Control+A", + "Alt+B", + "Shift+C", + "Win+D", + "control+e", // case insensitive + "ALT+F", + "SHIFT+G", + "WIN+H", + ]; + + validShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toBeNull(); + }); + }); + + it("should accept two modifiers with letter", () => { + const validShortcuts = [ + "Control+Alt+A", + "Control+Shift+B", + "Control+Win+C", + "Alt+Shift+D", + "Alt+Win+E", + "Shift+Win+F", + ]; + + validShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toBeNull(); + }); + }); + + it("should accept modifiers in different orders", () => { + const validShortcuts = ["Alt+Control+A", "Shift+Control+B", "Win+Alt+C"]; + + validShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toBeNull(); + }); + }); + }); + + describe("invalid shortcuts", () => { + it("should reject shortcuts without modifiers", () => { + const invalidShortcuts = ["A", "B", "Z", "1", "9"]; + + invalidShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + + it("should reject shortcuts with invalid base keys", () => { + const invalidShortcuts = [ + "Control+1", + "Alt+2", + "Shift+3", + "Win+4", + "Control+!", + "Alt+@", + "Shift+#", + "Win+$", + "Control+Space", + "Alt+Enter", + "Shift+Tab", + "Win+Escape", + ]; + + invalidShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + + it("should reject shortcuts with only modifiers", () => { + const invalidShortcuts = [ + "Control", + "Alt", + "Shift", + "Win", + "Control+Alt", + "Control+Shift", + "Alt+Shift", + "Control+Alt+Shift", + ]; + + invalidShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + + it("should reject shortcuts with invalid modifier names", () => { + const invalidShortcuts = ["Ctrl+A", "Command+A", "Super+A", "Meta+A", "Cmd+A", "Invalid+A"]; + + invalidShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + + it("should reject shortcuts with multiple base keys", () => { + const invalidShortcuts = ["Control+A+B", "Alt+Ctrl+Shift"]; + + invalidShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + + it("should reject shortcuts with more than two modifiers", () => { + const invalidShortcuts = [ + "Control+Alt+Shift+A", + "Control+Alt+Win+B", + "Control+Shift+Win+C", + "Alt+Shift+Win+D", + "Control+Alt+Shift+Win+E", + ]; + + invalidShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + + it("should reject shortcuts with extra characters", () => { + const invalidShortcuts = [ + "Control+A+", + "+Control+A", + "Control++A", + "Control+A+Extra", + "Control A", + "Control-A", + "Control.A", + ]; + + invalidShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + + it("should reject empty or whitespace shortcuts", () => { + // Empty string is handled by required validator + const controlEmpty = createControl(""); + expect(validator(controlEmpty)).toBeNull(); + + // Whitespace strings are invalid shortcuts + const whitespaceShortcuts = [" ", " ", "\t", "\n"]; + + whitespaceShortcuts.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + }); + + describe("edge cases", () => { + it("should handle null and undefined values", () => { + const controlNull = createControl(null); + const controlUndefined = createControl(undefined as any); + + expect(validator(controlNull)).toBeNull(); + expect(validator(controlUndefined)).toBeNull(); + }); + + it("should handle non-string values", () => { + const controlNumber = createControl(123 as any); + const controlObject = createControl({} as any); + const controlArray = createControl([] as any); + + expect(validator(controlNumber)).toEqual({ + invalidShortcut: { message: "Invalid shortcut" }, + }); + expect(validator(controlObject)).toEqual({ + invalidShortcut: { message: "Invalid shortcut" }, + }); + // Empty array becomes empty string when converted to string, which is handled by required validator + expect(validator(controlArray)).toBeNull(); + }); + + it("should handle very long strings", () => { + const longString = "Control+Alt+Shift+Win+A".repeat(100); + const control = createControl(longString); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + + describe("modifier combinations", () => { + it("should accept all possible single modifier combinations", () => { + const modifiers = ["Control", "Alt", "Shift", "Win"]; + + modifiers.forEach((modifier) => { + const control = createControl(`${modifier}+A`); + const result = validator(control); + expect(result).toBeNull(); + }); + }); + + it("should accept all possible two-modifier combinations", () => { + const combinations = [ + "Control+Alt+A", + "Control+Shift+A", + "Control+Win+A", + "Alt+Shift+A", + "Alt+Win+A", + "Shift+Win+A", + ]; + + combinations.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toBeNull(); + }); + }); + + it("should reject all three-modifier combinations", () => { + const combinations = [ + "Control+Alt+Shift+A", + "Control+Alt+Win+A", + "Control+Shift+Win+A", + "Alt+Shift+Win+A", + ]; + + combinations.forEach((shortcut) => { + const control = createControl(shortcut); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + + it("should reject all four modifiers combination", () => { + const control = createControl("Control+Alt+Shift+Win+A"); + const result = validator(control); + expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); + }); + }); + }); +}); diff --git a/apps/desktop/src/autofill/components/autotype-shortcut.component.ts b/apps/desktop/src/autofill/components/autotype-shortcut.component.ts new file mode 100644 index 00000000000..5cf1d90cb79 --- /dev/null +++ b/apps/desktop/src/autofill/components/autotype-shortcut.component.ts @@ -0,0 +1,139 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { + FormBuilder, + ReactiveFormsModule, + Validators, + ValidatorFn, + AbstractControl, + ValidationErrors, +} from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + AsyncActionsModule, + ButtonModule, + DialogModule, + DialogRef, + DialogService, + FormFieldModule, + IconButtonModule, +} from "@bitwarden/components"; + +@Component({ + templateUrl: "autotype-shortcut.component.html", + imports: [ + DialogModule, + CommonModule, + JslibModule, + ButtonModule, + IconButtonModule, + ReactiveFormsModule, + AsyncActionsModule, + FormFieldModule, + ], +}) +export class AutotypeShortcutComponent { + constructor( + private dialogRef: DialogRef, + private formBuilder: FormBuilder, + private i18nService: I18nService, + ) {} + + private shortcutArray: string[] = []; + + setShortcutForm = this.formBuilder.group({ + shortcut: ["", [Validators.required, this.shortcutCombinationValidator()]], + requireMasterPasswordOnClientRestart: true, + }); + + submit = async () => { + const shortcutFormControl = this.setShortcutForm.controls.shortcut; + + if (Utils.isNullOrWhitespace(shortcutFormControl.value) || shortcutFormControl.invalid) { + return; + } + + this.dialogRef.close(this.shortcutArray); + }; + + static open(dialogService: DialogService) { + return dialogService.open(AutotypeShortcutComponent); + } + + onShortcutKeydown(event: KeyboardEvent): void { + event.preventDefault(); + + const shortcut = this.buildShortcutFromEvent(event); + + if (shortcut != null) { + this.setShortcutForm.controls.shortcut.setValue(shortcut); + this.setShortcutForm.controls.shortcut.markAsDirty(); + this.setShortcutForm.controls.shortcut.updateValueAndValidity(); + } + } + + private buildShortcutFromEvent(event: KeyboardEvent): string | null { + const hasCtrl = event.ctrlKey; + const hasAlt = event.altKey; + const hasShift = event.shiftKey; + const hasMeta = event.metaKey; // Windows key on Windows, Command on macOS + + // Require at least one modifier (Control, Alt, Shift, or Super) + if (!hasCtrl && !hasAlt && !hasShift && !hasMeta) { + return null; + } + + const key = event.key; + + // Ignore pure modifier keys themselves + if (key === "Control" || key === "Alt" || key === "Shift" || key === "Meta") { + return null; + } + + // Accept a single alphabetical letter as the base key + const isAlphabetical = typeof key === "string" && /^[a-z]$/i.test(key); + if (!isAlphabetical) { + return null; + } + + const parts: string[] = []; + if (hasCtrl) { + parts.push("Control"); + } + if (hasAlt) { + parts.push("Alt"); + } + if (hasShift) { + parts.push("Shift"); + } + if (hasMeta) { + parts.push("Super"); + } + parts.push(key.toUpperCase()); + + this.shortcutArray = parts; + + return parts.join("+").replace("Super", "Win"); + } + + private shortcutCombinationValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = (control.value ?? "").toString(); + if (value.length === 0) { + return null; // handled by required + } + + // Must include exactly 1-2 modifiers and end with a single letter + // Valid examples: Ctrl+A, Shift+Z, Ctrl+Shift+X, Alt+Shift+Q + // Allow modifiers in any order, but only 1-2 modifiers total + const pattern = + /^(?=.*\b(Control|Alt|Shift|Win)\b)(?:Control\+|Alt\+|Shift\+|Win\+){1,2}[A-Z]$/i; + return pattern.test(value) + ? null + : { invalidShortcut: { message: this.i18nService.t("invalidShortcut") } }; + }; + } +} diff --git a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts index 884a4fc1ce4..09f03d2ef8e 100644 --- a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts @@ -5,21 +5,45 @@ import { LogService } from "@bitwarden/logging"; import { WindowMain } from "../../main/window.main"; import { stringIsNotUndefinedNullAndEmpty } from "../../utils"; +import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut"; export class MainDesktopAutotypeService { - keySequence: string = "CommandOrControl+Shift+B"; + autotypeKeyboardShortcut: AutotypeKeyboardShortcut; constructor( private logService: LogService, private windowMain: WindowMain, - ) {} + ) { + this.autotypeKeyboardShortcut = new AutotypeKeyboardShortcut(); + } init() { ipcMain.on("autofill.configureAutotype", (event, data) => { - if (data.enabled === true && !globalShortcut.isRegistered(this.keySequence)) { - this.enableAutotype(); - } else if (data.enabled === false && globalShortcut.isRegistered(this.keySequence)) { + if (data.enabled) { + const newKeyboardShortcut = new AutotypeKeyboardShortcut(); + const newKeyboardShortcutIsValid = newKeyboardShortcut.set(data.keyboardShortcut); + + if (newKeyboardShortcutIsValid) { + this.disableAutotype(); + this.autotypeKeyboardShortcut = newKeyboardShortcut; + this.enableAutotype(); + } else { + this.logService.error( + "Attempting to configure autotype but the shortcut given is invalid.", + ); + } + } else { this.disableAutotype(); + + // Deregister the incoming keyboard shortcut if needed + const setCorrectly = this.autotypeKeyboardShortcut.set(data.keyboardShortcut); + if ( + setCorrectly && + globalShortcut.isRegistered(this.autotypeKeyboardShortcut.getElectronFormat()) + ) { + globalShortcut.unregister(this.autotypeKeyboardShortcut.getElectronFormat()); + this.logService.info("Autotype disabled."); + } } }); @@ -30,34 +54,42 @@ export class MainDesktopAutotypeService { stringIsNotUndefinedNullAndEmpty(response.username) && stringIsNotUndefinedNullAndEmpty(response.password) ) { - this.doAutotype(response.username, response.password); + this.doAutotype( + response.username, + response.password, + this.autotypeKeyboardShortcut.getArrayFormat(), + ); } }); } disableAutotype() { - if (globalShortcut.isRegistered(this.keySequence)) { - globalShortcut.unregister(this.keySequence); + // Deregister the current keyboard shortcut if needed + const formattedKeyboardShortcut = this.autotypeKeyboardShortcut.getElectronFormat(); + if (globalShortcut.isRegistered(formattedKeyboardShortcut)) { + globalShortcut.unregister(formattedKeyboardShortcut); + this.logService.info("Autotype disabled."); } - - this.logService.info("Autotype disabled."); } private enableAutotype() { - const result = globalShortcut.register(this.keySequence, () => { - const windowTitle = autotype.getForegroundWindowTitle(); + const result = globalShortcut.register( + this.autotypeKeyboardShortcut.getElectronFormat(), + () => { + const windowTitle = autotype.getForegroundWindowTitle(); - this.windowMain.win.webContents.send("autofill.listenAutotypeRequest", { - windowTitle, - }); - }); + this.windowMain.win.webContents.send("autofill.listenAutotypeRequest", { + windowTitle, + }); + }, + ); result ? this.logService.info("Autotype enabled.") : this.logService.info("Enabling autotype failed."); } - private doAutotype(username: string, password: string) { + private doAutotype(username: string, password: string, keyboardShortcut: string[]) { const inputPattern = username + "\t" + password; const inputArray = new Array(inputPattern.length); @@ -65,6 +97,6 @@ export class MainDesktopAutotypeService { inputArray[i] = inputPattern.charCodeAt(i); } - autotype.typeInput(inputArray); + autotype.typeInput(inputArray, keyboardShortcut); } } diff --git a/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts b/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts new file mode 100644 index 00000000000..b26be92585e --- /dev/null +++ b/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts @@ -0,0 +1,98 @@ +import { defaultWindowsAutotypeKeyboardShortcut } from "../services/desktop-autotype.service"; + +/* + This class provides the following: + - A way to get and set an AutotypeKeyboardShortcut value within the main process + - A way to set an AutotypeKeyboardShortcut with validation + - A way to "get" the value in string array format or a single string format for electron + - Default shortcut support + + This is currently only supported for Windows operating systems. +*/ +export class AutotypeKeyboardShortcut { + private autotypeKeyboardShortcut: string[]; + + constructor() { + this.autotypeKeyboardShortcut = defaultWindowsAutotypeKeyboardShortcut; + } + + /* + Returns a boolean value indicating if the autotypeKeyboardShortcut + was valid and set or not. + */ + set(newAutotypeKeyboardShortcut: string[]) { + if (!this.#keyboardShortcutIsValid(newAutotypeKeyboardShortcut)) { + return false; + } + + this.autotypeKeyboardShortcut = newAutotypeKeyboardShortcut; + return true; + } + + /* + Returns the autotype keyboard shortcut as a string array. + */ + getArrayFormat() { + return this.autotypeKeyboardShortcut; + } + + /* + Returns the autotype keyboard shortcut as a single string, as + Electron expects. Please note this does not reorder the keys. + + See Electron keyboard shorcut docs for more info: + https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts + */ + getElectronFormat() { + return this.autotypeKeyboardShortcut.join("+"); + } + + /* + This private function validates the strArray input to make sure the array contains + valid, currently accepted shortcut keys for Windows. + + Valid windows shortcut keys: Control, Alt, Super, Shift, letters A - Z + Valid macOS shortcut keys: Control, Alt, Command, Shift, letters A - Z (not yet supported) + + See Electron keyboard shorcut docs for more info: + https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts + */ + #keyboardShortcutIsValid(strArray: string[]) { + const VALID_SHORTCUT_CONTROL_KEYS: string[] = ["Control", "Alt", "Super", "Shift"]; + const UNICODE_LOWER_BOUND = 65; // unicode 'A' + const UNICODE_UPPER_BOUND = 90; // unicode 'Z' + const MIN_LENGTH: number = 2; + const MAX_LENGTH: number = 3; + + // Ensure strArray is a string array of valid length + if ( + strArray === undefined || + strArray === null || + strArray.length < MIN_LENGTH || + strArray.length > MAX_LENGTH + ) { + return false; + } + + // Ensure strArray is all modifier keys, and that the last key is a letter + for (let i = 0; i < strArray.length; i++) { + if (i < strArray.length - 1) { + if (!VALID_SHORTCUT_CONTROL_KEYS.includes(strArray[i])) { + return false; + } + } else { + const unicodeValue: number = strArray[i].charCodeAt(0); + + if ( + Number.isNaN(unicodeValue) || + unicodeValue < UNICODE_LOWER_BOUND || + unicodeValue > UNICODE_UPPER_BOUND + ) { + return false; + } + } + } + + return true; + } +} diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index af238b17e80..fcb2f646743 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -127,8 +127,8 @@ export default { }, ); }, - configureAutotype: (enabled: boolean) => { - ipcRenderer.send("autofill.configureAutotype", { enabled }); + configureAutotype: (enabled: boolean, keyboardShortcut: string[]) => { + ipcRenderer.send("autofill.configureAutotype", { enabled, keyboardShortcut }); }, listenAutotypeRequest: ( fn: ( diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.ts index b156ffd3597..34f70be64cb 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.ts @@ -19,17 +19,36 @@ import { UserId } from "@bitwarden/user-core"; import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service"; +export const defaultWindowsAutotypeKeyboardShortcut: string[] = ["Control", "Shift", "B"]; + export const AUTOTYPE_ENABLED = new KeyDefinition( AUTOTYPE_SETTINGS_DISK, "autotypeEnabled", { deserializer: (b) => b }, ); +/* + Valid windows shortcut keys: Control, Alt, Super, Shift, letters A - Z + Valid macOS shortcut keys: Control, Alt, Command, Shift, letters A - Z + + See Electron keyboard shorcut docs for more info: + https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts +*/ +export const AUTOTYPE_KEYBOARD_SHORTCUT = new KeyDefinition( + AUTOTYPE_SETTINGS_DISK, + "autotypeKeyboardShortcut", + { deserializer: (b) => b }, +); + export class DesktopAutotypeService { private readonly autotypeEnabledState = this.globalStateProvider.get(AUTOTYPE_ENABLED); + private readonly autotypeKeyboardShortcut = this.globalStateProvider.get( + AUTOTYPE_KEYBOARD_SHORTCUT, + ); autotypeEnabledUserSetting$: Observable = of(false); resolvedAutotypeEnabled$: Observable = of(false); + autotypeKeyboardShortcut$: Observable = of(defaultWindowsAutotypeKeyboardShortcut); constructor( private accountService: AccountService, @@ -53,6 +72,9 @@ export class DesktopAutotypeService { } async init() { + this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$; + this.autotypeKeyboardShortcut$ = this.autotypeKeyboardShortcut.state$; + // Currently Autotype is only supported for Windows if (this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop) { // If `autotypeDefaultPolicy` is `true` for a user's organization, and the @@ -102,11 +124,11 @@ export class DesktopAutotypeService { ), ); - // When the resolvedAutotypeEnabled$ value changes, this might require - // hotkey registration / deregistration in the main process. - this.resolvedAutotypeEnabled$.subscribe((enabled) => { - ipc.autofill.configureAutotype(enabled); - }); + combineLatest([this.resolvedAutotypeEnabled$, this.autotypeKeyboardShortcut$]).subscribe( + ([resolvedAutotypeEnabled, autotypeKeyboardShortcut]) => { + ipc.autofill.configureAutotype(resolvedAutotypeEnabled, autotypeKeyboardShortcut); + }, + ); } } @@ -116,6 +138,10 @@ export class DesktopAutotypeService { }); } + async setAutotypeKeyboardShortcutState(keyboardShortcut: string[]): Promise { + await this.autotypeKeyboardShortcut.update(() => keyboardShortcut); + } + async matchCiphersToWindowTitle(windowTitle: string): Promise { const URI_PREFIX = "apptitle://"; windowTitle = windowTitle.toLowerCase(); diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index aa597a6bf97..08ec76af874 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4080,12 +4080,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4105,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." diff --git a/apps/desktop/src/scss/misc.scss b/apps/desktop/src/scss/misc.scss index 3c3d4ff508c..b64bdd92120 100644 --- a/apps/desktop/src/scss/misc.scss +++ b/apps/desktop/src/scss/misc.scss @@ -360,6 +360,13 @@ form, } } +.settings-link { + @include themify($themes) { + color: themed("primaryColor"); + } + font-weight: bold; +} + app-root > #loading, .loading { display: flex; From 3da89ee4e3eafc9d938d15b0a10e45a8bec42162 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Mon, 29 Sep 2025 10:28:55 -0400 Subject: [PATCH 24/55] [PM-25616] Refactor app-table-row-scrollable component to use openApplication (#16523) --- .../all-applications.component.html | 56 +++++++++---------- .../all-applications.component.ts | 6 -- .../app-table-row-scrollable.component.html | 28 +++++----- .../app-table-row-scrollable.component.ts | 2 +- .../critical-applications.component.html | 32 +++++------ .../critical-applications.component.ts | 6 -- 6 files changed, 60 insertions(+), 70 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html index b861925f7cb..febdb5fa0de 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html @@ -64,33 +64,33 @@
- } -
- - -
+
+ + +
- + + }
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts index 5aff3ed5304..51efafe501d 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts @@ -218,10 +218,4 @@ export class AllApplicationsComponent implements OnInit { this.selectedUrls.delete(applicationName); } }; - - isDrawerOpenForTableRow = (applicationName: string): boolean => { - // Note: This function will be replaced by PR #16523 with openApplication binding - // Using private access to BehaviorSubject value for backward compatibility - return (this.dataService as any).drawerDetailsSubject?.value?.invokerId === applicationName; - }; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html index 3d3a9baa6e8..ea41dd0aff3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html @@ -15,7 +15,7 @@ - + {{ row.applicationName }}
- } -
- -
+
+ +
- + + }
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts index 5cea6903568..9110779c980 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts @@ -201,10 +201,4 @@ export class CriticalApplicationsComponent implements OnInit { const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data); this.dataService.setDrawerForOrgAtRiskApps(data, invokerId); }; - - isDrawerOpenForTableRow = (applicationName: string) => { - // Note: This function will be replaced by PR #16523 with openApplication binding - // Using private access to BehaviorSubject value for backward compatibility - return (this.dataService as any).drawerDetailsSubject?.value?.invokerId === applicationName; - }; } From f9056b2711eda97c45b77325d86260b57e44d14f Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:09:41 +0200 Subject: [PATCH 25/55] [PM-22458] Ensure TOTP check ignores email or username fields (#16535) * [PM-22458] Ensure TOTP check ignores email or username fields * Resolve underlying totp error for shein.com Germany * Remove a guard check and add an optional chain --- apps/browser/src/autofill/services/autofill.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 89c3e2ee175..0e238d14d23 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -972,7 +972,7 @@ export default class AutofillService implements AutofillServiceInterface { fillScript.autosubmit = Array.from(formElementsSet); } - if (options.allowTotpAutofill) { + if (options.allowTotpAutofill && login?.totp) { await Promise.all( totps.map(async (t, i) => { if (Object.prototype.hasOwnProperty.call(filledFields, t.opid)) { @@ -980,10 +980,10 @@ export default class AutofillService implements AutofillServiceInterface { } filledFields[t.opid] = t; - const totpResponse = await firstValueFrom( - this.totpService.getCode$(options.cipher.login.totp), - ); + + const totpResponse = await firstValueFrom(this.totpService.getCode$(login.totp)); let totpValue = totpResponse.code; + if (totpValue.length == totps.length) { totpValue = totpValue.charAt(i); } From 645478af52484cb1c8a87a3fc8c78ff47a030746 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Mon, 29 Sep 2025 09:52:37 -0700 Subject: [PATCH 26/55] [PM-18629] Hide reorder icon when displaying only one custom field (#16460) --- .../components/custom-fields/custom-fields.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html index 4e3d9fb17d6..c5c1b752aef 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -107,7 +107,7 @@ (keydown)="handleKeyDown($event, field.value.name, i)" data-testid="reorder-toggle-button" [disabled]="parentFormDisabled" - *ngIf="canEdit(field.value.type)" + *ngIf="canEdit(field.value.type) && fields.controls.length > 1" >
From 83e8977d4b13895ceca8f0cbc06dc722c3b40c18 Mon Sep 17 00:00:00 2001 From: Tyler <71953103+fntyler@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:51:19 -0400 Subject: [PATCH 27/55] BRE-1040 Docker Compose rule (#16600) --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c1fb0b4794d..22c14f6b433 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -204,10 +204,11 @@ apps/web/src/locales/en/messages.json .github/workflows/release-desktop.yml @bitwarden/dept-bre .github/workflows/release-web.yml @bitwarden/dept-bre -## Docker files have shared ownership ## +## Docker-related files **/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre **/*.Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre **/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre +**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre **/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre ## Overrides From f988d3fd705767260a843c1940a8c18a6ef72d1e Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 29 Sep 2025 14:01:35 -0400 Subject: [PATCH 28/55] build(nx): continue on error for the experimental nx ci (#16646) --- .github/workflows/nx.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/nx.yml b/.github/workflows/nx.yml index 13d151225c2..9349239a134 100644 --- a/.github/workflows/nx.yml +++ b/.github/workflows/nx.yml @@ -38,4 +38,5 @@ jobs: uses: nrwl/nx-set-shas@826660b82addbef3abff5fa871492ebad618c9e1 # v4.3.3 - name: Run Nx affected tasks + continue-on-error: true run: npx nx affected -t build lint test \ No newline at end of file From c4ee2fdae2e6f3d3adbe6569d8739337344b0dd1 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:06:36 -0500 Subject: [PATCH 29/55] [PM-25982] Assign to Collections - My Items (#16591) * update cipher form to exclude my items collections * handle default collections for assign to collections and bulk * account for every returning true for empty arrays --- .../item-details-section.component.spec.ts | 41 +++++++- .../item-details-section.component.ts | 18 ++++ .../assign-collections.component.spec.ts | 96 ++++++++++++++++++- .../assign-collections.component.ts | 52 +++++++++- 4 files changed, 199 insertions(+), 8 deletions(-) diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index c41e58f679e..4da299ed039 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -7,7 +7,7 @@ import { BehaviorSubject, of } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { CollectionTypes, CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionType, CollectionTypes, CollectionView } from "@bitwarden/admin-console/common"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; @@ -33,6 +33,7 @@ const createMockCollection = ( organizationId: string, readOnly = false, canEdit = true, + type: CollectionType = CollectionTypes.DefaultUserCollection, ): CollectionView => { const cv = new CollectionView({ name, @@ -41,7 +42,7 @@ const createMockCollection = ( }); cv.readOnly = readOnly; cv.manage = true; - cv.type = CollectionTypes.DefaultUserCollection; + cv.type = type; cv.externalId = ""; cv.hidePasswords = false; cv.assigned = true; @@ -519,6 +520,42 @@ describe("ItemDetailsSectionComponent", () => { expect(component["collectionOptions"].map((c) => c.id)).toEqual(["col1", "col2", "col3"]); }); + + it("should exclude default collections when the cipher is only assigned to shared collections", async () => { + component.config.admin = false; + component.config.organizationDataOwnershipDisabled = true; + component.config.organizations = [{ id: "org1" } as Organization]; + component.config.collections = new Array(4) + .fill(null) + .map((_, i) => i + 1) + .map( + (i) => + createMockCollection( + `col${i}`, + `Collection ${i}`, + "org1", + false, + false, + i < 4 ? CollectionTypes.SharedCollection : CollectionTypes.DefaultUserCollection, + ) as CollectionView, + ); + component.originalCipherView = { + name: "cipher1", + organizationId: "org1", + folderId: "folder1", + collectionIds: ["col2", "col3"], + favorite: true, + } as CipherView; + fixture.detectChanges(); + await fixture.whenStable(); + + component.itemDetailsForm.controls.organizationId.setValue("org1"); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component["collectionOptions"].map((c) => c.id)).toEqual(["col1", "col2", "col3"]); + }); }); describe("readonlyCollections", () => { diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index 6af2fa19e26..ced6c809724 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -406,6 +406,17 @@ export class ItemDetailsSectionComponent implements OnInit { this.showCollectionsControl = true; } + /** + * Determine if the the cipher is only assigned to shared collections. + * i.e. The cipher is not assigned to a default collections. + * Note: `.every` will return true for an empty array + */ + const cipherIsOnlyInOrgCollections = + (this.originalCipherView?.collectionIds ?? []).length > 0 && + this.originalCipherView.collectionIds.every( + (cId) => + this.collections.find((c) => c.id === cId)?.type === CollectionTypes.SharedCollection, + ); this.collectionOptions = this.collections .filter((c) => { // The collection belongs to the organization @@ -423,10 +434,17 @@ export class ItemDetailsSectionComponent implements OnInit { return true; } + // When the cipher is only assigned to shared collections, do not allow a user to + // move it back to a default collection. Exclude the default collection from the list. + if (cipherIsOnlyInOrgCollections && c.type === CollectionTypes.DefaultUserCollection) { + return false; + } + // Non-admins can only select assigned collections that are not read only. (Non-AC) return c.assigned && !c.readOnly; }) .sort((a, b) => { + // Show default collection first const aIsDefaultCollection = a.type === CollectionTypes.DefaultUserCollection ? -1 : 0; const bIsDefaultCollection = b.type === CollectionTypes.DefaultUserCollection ? -1 : 0; return aIsDefaultCollection - bIsDefaultCollection; diff --git a/libs/vault/src/components/assign-collections.component.spec.ts b/libs/vault/src/components/assign-collections.component.spec.ts index e54bada30ba..414613e67d8 100644 --- a/libs/vault/src/components/assign-collections.component.spec.ts +++ b/libs/vault/src/components/assign-collections.component.spec.ts @@ -5,7 +5,11 @@ import { of } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { + CollectionService, + CollectionTypes, + CollectionView, +} from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -34,7 +38,6 @@ describe("AssignCollectionsComponent", () => { organizationId: "org-id" as OrganizationId, name: "Editable Collection", }); - editCollection.readOnly = false; editCollection.manage = true; @@ -52,6 +55,24 @@ describe("AssignCollectionsComponent", () => { }); readOnlyCollection2.readOnly = true; + const sharedCollection = new CollectionView({ + id: "shared-collection-id" as CollectionId, + organizationId: "org-id" as OrganizationId, + name: "Shared Collection", + }); + sharedCollection.readOnly = false; + sharedCollection.assigned = true; + sharedCollection.type = CollectionTypes.SharedCollection; + + const defaultCollection = new CollectionView({ + id: "default-collection-id" as CollectionId, + organizationId: "org-id" as OrganizationId, + name: "Default Collection", + }); + defaultCollection.readOnly = false; + defaultCollection.manage = true; + defaultCollection.type = CollectionTypes.DefaultUserCollection; + const params = { organizationId: "org-id" as OrganizationId, ciphers: [ @@ -116,4 +137,75 @@ describe("AssignCollectionsComponent", () => { ]); }); }); + + describe("default collections", () => { + const cipher1 = new CipherView(); + cipher1.id = "cipher-id-1"; + cipher1.collectionIds = [editCollection.id, sharedCollection.id]; + cipher1.edit = true; + + const cipher2 = new CipherView(); + cipher2.id = "cipher-id-2"; + cipher2.collectionIds = [defaultCollection.id]; + cipher2.edit = true; + + const cipher3 = new CipherView(); + cipher3.id = "cipher-id-3"; + cipher3.collectionIds = [defaultCollection.id]; + cipher3.edit = true; + + const cipher4 = new CipherView(); + cipher4.id = "cipher-id-4"; + cipher4.collectionIds = []; + cipher4.edit = true; + + it('does not show the "Default Collection" if any cipher is in a shared collection', async () => { + component.params = { + ...component.params, + ciphers: [cipher1, cipher2], + availableCollections: [editCollection, sharedCollection, defaultCollection], + }; + + await component.ngOnInit(); + fixture.detectChanges(); + + expect(component["availableCollections"].map((c) => c.id)).toEqual([ + editCollection.id, + sharedCollection.id, + ]); + }); + + it('shows the "Default Collection" if no ciphers are in a shared collection', async () => { + component.params = { + ...component.params, + ciphers: [cipher2, cipher3], + availableCollections: [editCollection, sharedCollection, defaultCollection], + }; + + await component.ngOnInit(); + fixture.detectChanges(); + + expect(component["availableCollections"].map((c) => c.id)).toEqual([ + editCollection.id, + sharedCollection.id, + defaultCollection.id, + ]); + }); + + it('shows the "Default Collection" for singular cipher', async () => { + component.params = { + ...component.params, + ciphers: [cipher4], + availableCollections: [readOnlyCollection1, sharedCollection, defaultCollection], + }; + + await component.ngOnInit(); + fixture.detectChanges(); + + expect(component["availableCollections"].map((c) => c.id)).toEqual([ + sharedCollection.id, + defaultCollection.id, + ]); + }); + }); }); diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index b2bd6e31ee5..453ba93f380 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -26,7 +26,11 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { + CollectionService, + CollectionTypes, + CollectionView, +} from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { getOrganizationById, @@ -311,9 +315,19 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI await this.setReadOnlyCollectionNames(); + const canAccessDefaultCollection = this.canAccessDefaultCollection( + this.params.availableCollections, + ); + this.availableCollections = this.params.availableCollections .filter((collection) => { - return collection.canEditItems(org); + if (canAccessDefaultCollection) { + return collection.canEditItems(org); + } + + return ( + collection.canEditItems(org) && collection.type !== CollectionTypes.DefaultUserCollection + ); }) .map((c) => ({ icon: "bwi-collection-shared", @@ -447,8 +461,16 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI const org = organizations.find((o) => o.id === orgId); this.orgName = org.name; - return collections.filter((c) => { - return c.organizationId === orgId && !c.readOnly; + const orgCollections = collections.filter((c) => c.organizationId === orgId); + + const canAccessDefaultCollection = this.canAccessDefaultCollection(collections); + + return orgCollections.filter((c) => { + if (canAccessDefaultCollection) { + return !c.readOnly; + } + + return !c.readOnly && c.type !== CollectionTypes.DefaultUserCollection; }); }), shareReplay({ refCount: true, bufferSize: 1 }), @@ -536,4 +558,26 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI }) .map((c) => c.name); } + + /** + * Determines if the ciphers to be assigned can be assigned to the Default Collection. + * When false, the Default Collections should be excluded from the list of available collections. + */ + private canAccessDefaultCollection(collections: CollectionView[]): boolean { + const collectionsObject = Object.fromEntries(collections.map((c) => [c.id, c])); + + const allCiphersUnassignedOrInDefault = this.params.ciphers.every( + (cipher) => + !cipher.collectionIds.length || + cipher.collectionIds.some( + (cId) => collectionsObject[cId]?.type === CollectionTypes.DefaultUserCollection, + ), + ); + + // When all ciphers are either: + // - unassigned + // - already in a Default Collection + // then the Default Collection can be shown. + return allCiphersUnassignedOrInDefault; + } } From 9aa2e2d99fd5edae74e4e91332206f68a4ddd77f Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 29 Sep 2025 14:57:29 -0400 Subject: [PATCH 30/55] build: add nx configuration in libs/node (#16550) --- libs/node/package.json | 1 + libs/node/project.json | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 libs/node/project.json diff --git a/libs/node/package.json b/libs/node/package.json index 823913c997d..6a375a2a5ad 100644 --- a/libs/node/package.json +++ b/libs/node/package.json @@ -13,6 +13,7 @@ }, "license": "GPL-3.0", "scripts": { + "test": "jest", "clean": "rimraf dist", "build": "npm run clean && tsc", "build:watch": "npm run clean && tsc -watch" diff --git a/libs/node/project.json b/libs/node/project.json new file mode 100644 index 00000000000..d7757cda2ad --- /dev/null +++ b/libs/node/project.json @@ -0,0 +1,41 @@ +{ + "name": "@bitwarden/node", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/node/src", + "projectType": "library", + "tags": ["scope:node", "type:lib"], + "targets": { + "build": { + "executor": "nx:run-script", + "dependsOn": [], + "options": { + "script": "build" + } + }, + "build:watch": { + "executor": "nx:run-script", + "options": { + "script": "build:watch" + } + }, + "clean": { + "executor": "nx:run-script", + "options": { + "script": "clean" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/node/**/*.ts"] + } + }, + "test": { + "executor": "nx:run-script", + "options": { + "script": "test" + } + } + } +} From 4f79cc8c522c0bd079f2598b6286c75fc01f192e Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 29 Sep 2025 14:23:59 -0500 Subject: [PATCH 31/55] PM-26073 remove learn more (#16571) --- .../app/dirt/access-intelligence/all-activity.component.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html index 8459dc565ae..35c9fb451e6 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html @@ -12,11 +12,6 @@ {{ "noAppsInOrgTitle" | i18n: organization?.name }}
- - - } From e784622f67dc6a3f312c86cca63c768ce4f0fd35 Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:37:23 -0500 Subject: [PATCH 32/55] [PM-25613] Add report trigger logic (#16615) * Add password trigger logic to report service. Also updated api to use classes that properly handle encstring with placeholders for upcoming usage * Fix merged test case conflict * Fix type errors and test cases. Make create data functions for report and summary --- .../helpers/risk-insights-data-mappers.ts | 37 ++- .../src/dirt/reports/risk-insights/index.ts | 1 + .../risk-insights/models/api-models.types.ts | 75 ++++- .../reports/risk-insights/models/index.ts | 3 + .../risk-insights/models/password-health.ts | 11 +- .../risk-insights/models/report-models.ts | 7 - .../services/password-health.service.ts | 1 + .../risk-insights-api.service.spec.ts | 299 +++++++++--------- .../services/risk-insights-api.service.ts | 47 +-- .../services/risk-insights-data.service.ts | 133 +++++++- .../risk-insights-encryption.service.spec.ts | 4 +- .../risk-insights-encryption.service.ts | 12 +- .../risk-insights-report.service.spec.ts | 152 ++++++--- .../services/risk-insights-report.service.ts | 206 ++++++------ .../all-applications.component.ts | 13 +- .../risk-insights.component.ts | 3 + 16 files changed, 630 insertions(+), 374 deletions(-) create mode 100644 bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/index.ts diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts index 8fbe45efeab..3f679924df9 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts @@ -6,7 +6,11 @@ import { LEGACY_CipherHealthReportDetail, LEGACY_CipherHealthReportUriDetail, } from "../models/password-health"; -import { ApplicationHealthReportDetail } from "../models/report-models"; +import { + ApplicationHealthReportDetail, + OrganizationReportSummary, + RiskInsightsReportData, +} from "../models/report-models"; import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response"; export function flattenMemberDetails( @@ -144,3 +148,34 @@ export function getApplicationReportDetail( return reportDetail; } + +/** + * Create a new Risk Insights Report + * + * @returns An empty report + */ +export function createNewReportData(): RiskInsightsReportData { + return { + data: [], + summary: createNewSummaryData(), + }; +} + +/** + * Create a new Risk Insights Report Summary + * + * @returns An empty report summary + */ +export function createNewSummaryData(): OrganizationReportSummary { + return { + totalMemberCount: 0, + totalAtRiskMemberCount: 0, + totalApplicationCount: 0, + totalAtRiskApplicationCount: 0, + totalCriticalMemberCount: 0, + totalCriticalAtRiskMemberCount: 0, + totalCriticalApplicationCount: 0, + totalCriticalAtRiskApplicationCount: 0, + newApplications: [], + }; +} diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/index.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/index.ts index b2221a94a89..d6295daa03f 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/index.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/index.ts @@ -1 +1,2 @@ export * from "./services"; +export * from "./models"; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts index 1386b2d044f..89293651a23 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts @@ -1,7 +1,10 @@ -import { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { PasswordHealthReportApplicationId, RiskInsightsReport } from "./report-models"; +import { createNewSummaryData } from "../helpers"; + +import { OrganizationReportSummary, PasswordHealthReportApplicationId } from "./report-models"; // -------------------- Password Health Report Models -------------------- /** @@ -32,18 +35,76 @@ export interface PasswordHealthReportApplicationsRequest { // -------------------- Risk Insights Report Models -------------------- export interface SaveRiskInsightsReportRequest { - data: RiskInsightsReport; + data: { + organizationId: OrganizationId; + date: string; + reportData: string; + contentEncryptionKey: string; + }; } -export interface SaveRiskInsightsReportResponse { +export class SaveRiskInsightsReportResponse extends BaseResponse { id: string; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("organizationId"); + } +} +export function isSaveRiskInsightsReportResponse(obj: any): obj is SaveRiskInsightsReportResponse { + return obj && typeof obj.id === "string" && obj.id !== ""; } -export interface GetRiskInsightsReportResponse { +export class GetRiskInsightsReportResponse extends BaseResponse { id: string; organizationId: OrganizationId; // TODO Update to use creationDate from server date: string; - reportData: EncryptedString; - contentEncryptionKey: EncryptedString; + reportData: EncString; + contentEncryptionKey: EncString; + + constructor(response: any) { + super(response); + + this.id = this.getResponseProperty("organizationId"); + this.organizationId = this.getResponseProperty("organizationId"); + this.date = this.getResponseProperty("date"); + this.reportData = new EncString(this.getResponseProperty("reportData")); + this.contentEncryptionKey = new EncString(this.getResponseProperty("contentEncryptionKey")); + } +} + +export class GetRiskInsightsSummaryResponse extends BaseResponse { + id: string; + organizationId: OrganizationId; + encryptedData: EncString; // Decrypted as OrganizationReportSummary + contentEncryptionKey: EncString; + + constructor(response: any) { + super(response); + // TODO Handle taking array of summary data and converting to array + this.id = this.getResponseProperty("id"); + this.organizationId = this.getResponseProperty("organizationId"); + this.encryptedData = this.getResponseProperty("encryptedData"); + this.contentEncryptionKey = this.getResponseProperty("contentEncryptionKey"); + } + + // TODO + getSummary(): OrganizationReportSummary { + return createNewSummaryData(); + } +} +export class GetRiskInsightsApplicationDataResponse extends BaseResponse { + reportId: string; + organizationId: OrganizationId; + encryptedData: EncString; + contentEncryptionKey: EncString; + + constructor(response: any) { + super(response); + this.reportId = this.getResponseProperty("reportId"); + this.organizationId = this.getResponseProperty("organizationId"); + this.encryptedData = this.getResponseProperty("encryptedData"); + this.contentEncryptionKey = this.getResponseProperty("contentEncryptionKey"); + } } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/index.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/index.ts new file mode 100644 index 00000000000..b8fcfe251ff --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/index.ts @@ -0,0 +1,3 @@ +export * from "./api-models.types"; +export * from "./password-health"; +export * from "./report-models"; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts index 872e883e42e..e026a4475b7 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts @@ -1,9 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeVariant } from "@bitwarden/components"; -import { EncString } from "@bitwarden/sdk-internal"; import { ApplicationHealthReportDetail } from "./report-models"; @@ -40,7 +40,7 @@ export type ExposedPasswordDetail = { export interface EncryptedDataWithKey { organizationId: OrganizationId; encryptedData: EncString; - encryptionKey: EncString; + contentEncryptionKey: EncString; } export type LEGACY_MemberDetailsFlat = { @@ -76,10 +76,3 @@ export type LEGACY_CipherHealthReportUriDetail = { trimmedUri: string; cipher: CipherView; }; - -export interface EncryptedDataModel { - organizationId: OrganizationId; - encryptedData: string; - encryptionKey: string; - date: Date; -} diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts index acbec1592a0..1758bb41b1b 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts @@ -1,6 +1,5 @@ import { Opaque } from "type-fest"; -import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeVariant } from "@bitwarden/components"; @@ -154,12 +153,6 @@ export interface RiskInsightsReportData { data: ApplicationHealthReportDetailEnriched[]; summary: OrganizationReportSummary; } -export interface RiskInsightsReport { - organizationId: OrganizationId; - date: string; - reportData: string; - reportKey: string; -} export type ReportScore = { label: string; badgeVariant: BadgeVariant; sortOrder: number }; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/password-health.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/password-health.service.ts index 865f5cd712f..3904c4c3865 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/password-health.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/password-health.service.ts @@ -32,6 +32,7 @@ export class PasswordHealthService { .passwordLeaked(cipher.login.password) .then((exposedCount) => ({ cipher, exposedCount })), ), + // [FIXME] ExposedDetails is can still return a null filter(({ exposedCount }) => exposedCount > 0), map(({ cipher, exposedCount }) => ({ exposedXTimes: exposedCount, diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts index ffcdceadad7..4eda92f0eb3 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts @@ -1,11 +1,20 @@ import { mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { makeEncString } from "@bitwarden/common/spec"; import { OrganizationId, OrganizationReportId } from "@bitwarden/common/types/guid"; -import { SaveRiskInsightsReportRequest } from "../models/api-models.types"; -import { EncryptedDataModel } from "../models/password-health"; +import { + GetRiskInsightsApplicationDataResponse, + GetRiskInsightsReportResponse, + GetRiskInsightsSummaryResponse, + SaveRiskInsightsReportRequest, + SaveRiskInsightsReportResponse, +} from "../models/api-models.types"; +import { EncryptedDataWithKey } from "../models/password-health"; import { RiskInsightsApiService } from "./risk-insights-api.service"; @@ -13,14 +22,11 @@ describe("RiskInsightsApiService", () => { let service: RiskInsightsApiService; const mockApiService = mock(); + const mockId = "id"; const orgId = "org1" as OrganizationId; - - const getRiskInsightsReportResponse = { - organizationId: orgId, - date: new Date().toISOString(), - reportData: "test", - reportKey: "test-key", - }; + const mockReportId = "report-1"; + const mockKey = "encryption-key-1"; + const mockData = "encrypted-data"; const reportData = makeEncString("test").encryptedString?.toString() ?? ""; const reportKey = makeEncString("test-key").encryptedString?.toString() ?? ""; @@ -30,12 +36,9 @@ describe("RiskInsightsApiService", () => { organizationId: orgId, date: new Date().toISOString(), reportData: reportData, - reportKey: reportKey, + contentEncryptionKey: reportKey, }, }; - const saveRiskInsightsReportResponse = { - ...saveRiskInsightsReportRequest.data, - }; beforeEach(() => { service = new RiskInsightsApiService(mockApiService); @@ -45,11 +48,19 @@ describe("RiskInsightsApiService", () => { expect(service).toBeTruthy(); }); - it("Get Report: should call apiService.send with correct parameters and return the response for getRiskInsightsReport ", (done) => { + it("getRiskInsightsReport$ should call apiService.send with correct parameters and return the response", () => { + const getRiskInsightsReportResponse = { + id: mockId, + organizationId: orgId, + date: new Date().toISOString(), + reportData: mockData, + contentEncryptionKey: mockKey, + }; + mockApiService.send.mockReturnValue(Promise.resolve(getRiskInsightsReportResponse)); service.getRiskInsightsReport$(orgId).subscribe((result) => { - expect(result).toEqual(getRiskInsightsReportResponse); + expect(result).toEqual(new GetRiskInsightsReportResponse(getRiskInsightsReportResponse)); expect(mockApiService.send).toHaveBeenCalledWith( "GET", `/reports/organizations/${orgId.toString()}/latest`, @@ -57,187 +68,173 @@ describe("RiskInsightsApiService", () => { true, true, ); - done(); }); }); - it("Get Report: should return null if apiService.send rejects with 404 error for getRiskInsightsReport", (done) => { - const error = { statusCode: 404 }; - mockApiService.send.mockReturnValue(Promise.reject(error)); + it("getRiskInsightsReport$ should return null if apiService.send rejects with 404 error", async () => { + const mockError = new ErrorResponse(null, 404); + mockApiService.send.mockReturnValue(Promise.reject(mockError)); - service.getRiskInsightsReport$(orgId).subscribe((result) => { - expect(result).toBeNull(); - done(); - }); + const result = await firstValueFrom(service.getRiskInsightsReport$(orgId)); + + expect(result).toBeNull(); }); - it("Get Report: should throw error if apiService.send rejects with non-404 error for getRiskInsightsReport", (done) => { + it("getRiskInsightsReport$ should propagate errors if apiService.send rejects 500 server error", async () => { const error = { statusCode: 500, message: "Server error" }; mockApiService.send.mockReturnValue(Promise.reject(error)); - service.getRiskInsightsReport$(orgId).subscribe({ - next: () => { - // Should not reach here - fail("Expected error to be thrown"); - }, - error: () => { - expect(mockApiService.send).toHaveBeenCalledWith( - "GET", - `/reports/organizations/${orgId.toString()}/latest`, - null, - true, - true, - ); - done(); - }, - complete: () => { - done(); - }, - }); + await expect(firstValueFrom(service.getRiskInsightsReport$(orgId))).rejects.toEqual(error); + + expect(mockApiService.send).toHaveBeenCalledWith( + "GET", + `/reports/organizations/${orgId.toString()}/latest`, + null, + true, + true, + ); }); - it("Save Report: should call apiService.send with correct parameters for saveRiskInsightsReport", (done) => { - mockApiService.send.mockReturnValue(Promise.resolve(saveRiskInsightsReportResponse)); + it("saveRiskInsightsReport$ should call apiService.send with correct parameters", async () => { + mockApiService.send.mockReturnValue(Promise.resolve(saveRiskInsightsReportRequest)); - service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId).subscribe((result) => { - expect(result).toEqual(saveRiskInsightsReportResponse); - expect(mockApiService.send).toHaveBeenCalledWith( - "POST", - `/reports/organizations/${orgId.toString()}`, - saveRiskInsightsReportRequest.data, - true, - true, - ); - done(); - }); + const result = await firstValueFrom( + service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId), + ); + + expect(result).toEqual(new SaveRiskInsightsReportResponse(saveRiskInsightsReportRequest)); + expect(mockApiService.send).toHaveBeenCalledWith( + "POST", + `/reports/organizations/${orgId.toString()}`, + saveRiskInsightsReportRequest.data, + true, + true, + ); }); - it("Save Report: should propagate errors from apiService.send for saveRiskInsightsReport - 1", (done) => { + it("saveRiskInsightsReport$ should propagate errors from apiService.send for saveRiskInsightsReport - 1", async () => { const error = { statusCode: 500, message: "Internal Server Error" }; mockApiService.send.mockReturnValue(Promise.reject(error)); - service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId).subscribe({ - next: () => { - fail("Expected error to be thrown"); - }, - error: () => { - expect(mockApiService.send).toHaveBeenCalledWith( - "POST", - `/reports/organizations/${orgId.toString()}`, - saveRiskInsightsReportRequest.data, - true, - true, - ); - done(); - }, - complete: () => { - done(); - }, - }); + await expect( + firstValueFrom(service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId)), + ).rejects.toEqual(error); + + expect(mockApiService.send).toHaveBeenCalledWith( + "POST", + `/reports/organizations/${orgId.toString()}`, + saveRiskInsightsReportRequest.data, + true, + true, + ); }); - it("Save Report: should propagate network errors from apiService.send for saveRiskInsightsReport - 2", (done) => { + it("saveRiskInsightsReport$ should propagate network errors from apiService.send - 2", async () => { const error = new Error("Network error"); mockApiService.send.mockReturnValue(Promise.reject(error)); - service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId).subscribe({ - next: () => { - fail("Expected error to be thrown"); - }, - error: () => { - expect(mockApiService.send).toHaveBeenCalledWith( - "POST", - `/reports/organizations/${orgId.toString()}`, - saveRiskInsightsReportRequest.data, - true, - true, - ); - done(); - }, - complete: () => { - done(); - }, - }); + await expect( + firstValueFrom(service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId)), + ).rejects.toEqual(error); + + expect(mockApiService.send).toHaveBeenCalledWith( + "POST", + `/reports/organizations/${orgId.toString()}`, + saveRiskInsightsReportRequest.data, + true, + true, + ); }); - it("Get Summary: should call apiService.send with correct parameters and return an Observable", (done) => { + it("getRiskInsightsSummary$ should call apiService.send with correct parameters and return an Observable", async () => { const minDate = new Date("2024-01-01"); const maxDate = new Date("2024-01-31"); - const mockResponse: EncryptedDataModel[] = [{ encryptedData: "abc" } as EncryptedDataModel]; + const mockResponse = [ + { + reportId: mockReportId, + organizationId: orgId, + encryptedData: mockData, + contentEncryptionKey: mockKey, + }, + ]; mockApiService.send.mockResolvedValueOnce(mockResponse); - service.getRiskInsightsSummary$(orgId, minDate, maxDate).subscribe((result) => { - expect(mockApiService.send).toHaveBeenCalledWith( - "GET", - `/reports/organizations/${orgId.toString()}/data/summary?startDate=${minDate.toISOString().split("T")[0]}&endDate=${maxDate.toISOString().split("T")[0]}`, - null, - true, - true, - ); - expect(result).toEqual(mockResponse); - done(); - }); + const result = await firstValueFrom(service.getRiskInsightsSummary$(orgId, minDate, maxDate)); + + expect(mockApiService.send).toHaveBeenCalledWith( + "GET", + `/reports/organizations/${orgId.toString()}/data/summary?startDate=${minDate.toISOString().split("T")[0]}&endDate=${maxDate.toISOString().split("T")[0]}`, + null, + true, + true, + ); + expect(result).toEqual(new GetRiskInsightsSummaryResponse(mockResponse)); }); - it("Update Summary: should call apiService.send with correct parameters and return an Observable", (done) => { - const data: EncryptedDataModel = { encryptedData: "xyz" } as EncryptedDataModel; + it("updateRiskInsightsSummary$ should call apiService.send with correct parameters and return an Observable", async () => { + const data: EncryptedDataWithKey = { + organizationId: orgId, + encryptedData: new EncString(mockData), + contentEncryptionKey: new EncString(mockKey), + }; + const reportId = "report123" as OrganizationReportId; mockApiService.send.mockResolvedValueOnce(undefined); - service.updateRiskInsightsSummary$(data, orgId, reportId).subscribe((result) => { - expect(mockApiService.send).toHaveBeenCalledWith( - "PATCH", - `/reports/organizations/${orgId.toString()}/data/summary/${reportId.toString()}`, - data, - true, - true, - ); - expect(result).toBeUndefined(); - done(); - }); + const result = await firstValueFrom(service.updateRiskInsightsSummary$(data, orgId, reportId)); + + expect(mockApiService.send).toHaveBeenCalledWith( + "PATCH", + `/reports/organizations/${orgId.toString()}/data/summary/${reportId.toString()}`, + data, + true, + true, + ); + expect(result).toBeUndefined(); }); - it("Get Applications: should call apiService.send with correct parameters and return an Observable", (done) => { + it("getRiskInsightsApplicationData$ should call apiService.send with correct parameters and return an Observable", async () => { const reportId = "report123" as OrganizationReportId; - const mockResponse: EncryptedDataModel | null = { - encryptedData: "abc", - } as EncryptedDataModel; + const mockResponse: EncryptedDataWithKey | null = { + organizationId: orgId, + encryptedData: new EncString(mockData), + contentEncryptionKey: new EncString(mockKey), + }; mockApiService.send.mockResolvedValueOnce(mockResponse); - service.getRiskInsightsApplicationData$(orgId, reportId).subscribe((result) => { - expect(mockApiService.send).toHaveBeenCalledWith( - "GET", - `/reports/organizations/${orgId.toString()}/data/application/${reportId.toString()}`, - null, - true, - true, - ); - expect(result).toEqual(mockResponse); - done(); - }); + const result = await firstValueFrom(service.getRiskInsightsApplicationData$(orgId, reportId)); + expect(mockApiService.send).toHaveBeenCalledWith( + "GET", + `/reports/organizations/${orgId.toString()}/data/application/${reportId.toString()}`, + null, + true, + true, + ); + expect(result).toEqual(new GetRiskInsightsApplicationDataResponse(mockResponse)); }); - it("Update Applications: should call apiService.send with correct parameters and return an Observable", (done) => { - const applicationData: EncryptedDataModel = { encryptedData: "xyz" } as EncryptedDataModel; + it("updateRiskInsightsApplicationData$ should call apiService.send with correct parameters and return an Observable", async () => { + const applicationData: EncryptedDataWithKey = { + organizationId: orgId, + encryptedData: new EncString(mockData), + contentEncryptionKey: new EncString(mockKey), + }; const reportId = "report123" as OrganizationReportId; mockApiService.send.mockResolvedValueOnce(undefined); - - service - .updateRiskInsightsApplicationData$(applicationData, orgId, reportId) - .subscribe((result) => { - expect(mockApiService.send).toHaveBeenCalledWith( - "PATCH", - `/reports/organizations/${orgId.toString()}/data/application/${reportId.toString()}`, - applicationData, - true, - true, - ); - expect(result).toBeUndefined(); - done(); - }); + const result = await firstValueFrom( + service.updateRiskInsightsApplicationData$(applicationData, orgId, reportId), + ); + expect(mockApiService.send).toHaveBeenCalledWith( + "PATCH", + `/reports/organizations/${orgId.toString()}/data/application/${reportId.toString()}`, + applicationData, + true, + true, + ); + expect(result).toBeUndefined(); }); }); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts index 9d8c2291749..8f40ae91b47 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts @@ -1,29 +1,38 @@ -import { from, Observable } from "rxjs"; +import { catchError, from, map, Observable, of, throwError } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { OrganizationId, OrganizationReportId } from "@bitwarden/common/types/guid"; import { + GetRiskInsightsApplicationDataResponse, GetRiskInsightsReportResponse, + GetRiskInsightsSummaryResponse, SaveRiskInsightsReportRequest, SaveRiskInsightsReportResponse, } from "../models/api-models.types"; -import { EncryptedDataModel } from "../models/password-health"; +import { EncryptedDataWithKey } from "../models/password-health"; export class RiskInsightsApiService { constructor(private apiService: ApiService) {} getRiskInsightsReport$(orgId: OrganizationId): Observable { - const dbResponse = this.apiService - .send("GET", `/reports/organizations/${orgId.toString()}/latest`, null, true, true) - .catch((error: any): any => { - if (error.statusCode === 404) { - return null; // Handle 404 by returning null or an appropriate default value + const dbResponse = this.apiService.send( + "GET", + `/reports/organizations/${orgId.toString()}/latest`, + null, + true, + true, + ); + return from(dbResponse).pipe( + map((response) => new GetRiskInsightsReportResponse(response)), + catchError((error: unknown) => { + if (error instanceof ErrorResponse && error.statusCode === 404) { + return of(null); // Handle 404 by returning null or an appropriate default value } - throw error; // Re-throw other errors - }); - - return from(dbResponse as Promise); + return throwError(() => error); // Re-throw other errors + }), + ); } saveRiskInsightsReport$( @@ -38,14 +47,14 @@ export class RiskInsightsApiService { true, ); - return from(dbResponse as Promise); + return from(dbResponse).pipe(map((response) => new SaveRiskInsightsReportResponse(response))); } getRiskInsightsSummary$( orgId: string, minDate: Date, maxDate: Date, - ): Observable { + ): Observable { const minDateStr = minDate.toISOString().split("T")[0]; const maxDateStr = maxDate.toISOString().split("T")[0]; const dbResponse = this.apiService.send( @@ -56,11 +65,11 @@ export class RiskInsightsApiService { true, ); - return from(dbResponse as Promise); + return from(dbResponse).pipe(map((response) => new GetRiskInsightsSummaryResponse(response))); } updateRiskInsightsSummary$( - summaryData: EncryptedDataModel, + summaryData: EncryptedDataWithKey, organizationId: OrganizationId, reportId: OrganizationReportId, ): Observable { @@ -78,7 +87,7 @@ export class RiskInsightsApiService { getRiskInsightsApplicationData$( orgId: OrganizationId, reportId: OrganizationReportId, - ): Observable { + ): Observable { const dbResponse = this.apiService.send( "GET", `/reports/organizations/${orgId.toString()}/data/application/${reportId.toString()}`, @@ -87,11 +96,13 @@ export class RiskInsightsApiService { true, ); - return from(dbResponse as Promise); + return from(dbResponse).pipe( + map((response) => new GetRiskInsightsApplicationDataResponse(response)), + ); } updateRiskInsightsApplicationData$( - applicationData: EncryptedDataModel, + applicationData: EncryptedDataWithKey, orgId: OrganizationId, reportId: OrganizationReportId, ): Observable { diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts index cc90fb6940a..7038844998d 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts @@ -1,5 +1,14 @@ -import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; -import { finalize, switchMap, withLatestFrom, map } from "rxjs/operators"; +import { BehaviorSubject, EMPTY, firstValueFrom, Observable, of } from "rxjs"; +import { + distinctUntilChanged, + exhaustMap, + filter, + finalize, + map, + switchMap, + tap, + withLatestFrom, +} from "rxjs/operators"; import { getOrganizationById, @@ -17,6 +26,7 @@ import { DrawerDetails, ApplicationHealthReportDetail, ApplicationHealthReportDetailEnriched, + ReportDetailsAndSummary, } from "../models/report-models"; import { CriticalAppsService } from "./critical-apps.service"; @@ -66,6 +76,15 @@ export class RiskInsightsDataService { }); drawerDetails$ = this.drawerDetailsSubject.asObservable(); + // ------------------------- Report Variables ---------------- + // The last run report details + private reportResultsSubject = new BehaviorSubject(null); + reportResults$ = this.reportResultsSubject.asObservable(); + // Is a report being generated + private isRunningReportSubject = new BehaviorSubject(false); + isRunningReport$ = this.isRunningReportSubject.asObservable(); + // The error from report generation if there was an error + constructor( private accountService: AccountService, private criticalAppsService: CriticalAppsService, @@ -81,7 +100,7 @@ export class RiskInsightsDataService { this.userIdSubject.next(userId); } - // [FIXME] getOrganizationById is now deprecated - update when we can + // [FIXME] getOrganizationById is now deprecated - replace with appropriate method // Fetch organization details const org = await firstValueFrom( this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)), @@ -96,20 +115,18 @@ export class RiskInsightsDataService { // Load critical applications for organization await this.criticalAppsService.loadOrganizationContext(organizationId, userId); - // TODO: PM-25613 - // // Load existing report + // Load existing report + this.fetchLastReport(organizationId, userId); - // this.fetchLastReport(organizationId, userId); - - // // Setup new report generation - // this._runApplicationsReport().subscribe({ - // next: (result) => { - // this.isRunningReportSubject.next(false); - // }, - // error: () => { - // this.errorSubject.next("Failed to save report"); - // }, - // }); + // Setup new report generation + this._runApplicationsReport().subscribe({ + next: (result) => { + this.isRunningReportSubject.next(false); + }, + error: () => { + this.errorSubject.next("Failed to save report"); + }, + }); } /** @@ -276,4 +293,88 @@ export class RiskInsightsDataService { }); } }; + + // ------------------- Trigger Report Generation ------------------- + /** Trigger generating a report based on the current applications */ + triggerReport(): void { + this.isRunningReportSubject.next(true); + } + + /** + * Fetches the applications report and updates the applicationsSubject. + * @param organizationId The ID of the organization. + */ + fetchLastReport(organizationId: OrganizationId, userId: UserId): void { + this.isLoadingSubject.next(true); + + this.reportService + .getRiskInsightsReport$(organizationId, userId) + .pipe( + switchMap((report) => { + return this.enrichReportData$(report.data).pipe( + map((enrichedReport) => ({ + data: enrichedReport, + summary: report.summary, + })), + ); + }), + finalize(() => { + this.isLoadingSubject.next(false); + }), + ) + .subscribe({ + next: ({ data, summary }) => { + this.reportResultsSubject.next({ + data, + summary, + dateCreated: new Date(), + }); + this.errorSubject.next(null); + this.isLoadingSubject.next(false); + }, + error: () => { + this.errorSubject.next("Failed to fetch report"); + this.reportResultsSubject.next(null); + this.isLoadingSubject.next(false); + }, + }); + } + + private _runApplicationsReport() { + return this.isRunningReport$.pipe( + distinctUntilChanged(), + filter((isRunning) => isRunning), + withLatestFrom(this.organizationDetails$, this.userId$), + exhaustMap(([_, organizationDetails, userId]) => { + const organizationId = organizationDetails?.organizationId; + if (!organizationId || !userId) { + return EMPTY; + } + + // Generate the report + return this.reportService.generateApplicationsReport$(organizationId).pipe( + map((data) => ({ + data, + summary: this.reportService.generateApplicationsSummary(data), + })), + switchMap(({ data, summary }) => + this.enrichReportData$(data).pipe( + map((enrichedData) => ({ data: enrichedData, summary })), + ), + ), + tap(({ data, summary }) => { + this.reportResultsSubject.next({ data, summary, dateCreated: new Date() }); + this.errorSubject.next(null); + }), + switchMap(({ data, summary }) => { + // Just returns ID + return this.reportService.saveRiskInsightsReport$(data, summary, { + organizationId, + userId, + }); + }), + ); + }), + ); + } } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.spec.ts index dae56327c29..9b7bb3b7258 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.spec.ts @@ -70,8 +70,8 @@ describe("RiskInsightsEncryptionService", () => { ); expect(result).toEqual({ organizationId: orgId, - encryptedData: ENCRYPTED_TEXT, - encryptionKey: ENCRYPTED_KEY, + encryptedData: new EncString(ENCRYPTED_TEXT), + contentEncryptionKey: new EncString(ENCRYPTED_KEY), }); }); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.ts index f3c3a68b470..7bf01b04a63 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.ts @@ -51,13 +51,13 @@ export class RiskInsightsEncryptionService { throw new Error("Encryption failed, encrypted strings are null"); } - const encryptedData = dataEncrypted.encryptedString; - const encryptionKey = wrappedEncryptionKey.encryptedString; + const encryptedData = dataEncrypted; + const contentEncryptionKeyString = wrappedEncryptionKey; - const encryptedDataPacket = { - organizationId: organizationId, - encryptedData: encryptedData, - encryptionKey: encryptionKey, + const encryptedDataPacket: EncryptedDataWithKey = { + organizationId, + encryptedData, + contentEncryptionKey: contentEncryptionKeyString, }; return encryptedDataPacket; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts index 6c6fbb5b92c..18836fb1319 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts @@ -1,13 +1,24 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { GetRiskInsightsReportResponse } from "../models/api-models.types"; +import { createNewSummaryData } from "../helpers"; +import { + GetRiskInsightsReportResponse, + SaveRiskInsightsReportResponse, +} from "../models/api-models.types"; +import { EncryptedDataWithKey } from "../models/password-health"; +import { + ApplicationHealthReportDetail, + OrganizationReportSummary, + RiskInsightsReportData, +} from "../models/report-models"; import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response"; import { mockCiphers } from "./ciphers.mock"; @@ -31,10 +42,20 @@ describe("RiskInsightsReportService", () => { decryptRiskInsightsReport: jest.fn().mockResolvedValue("decryptedReportData"), }); - // Mock data - const mockOrgId = "orgId" as OrganizationId; + // Non changing mock data + const mockOrganizationId = "orgId" as OrganizationId; + const mockUserId = "userId" as UserId; + const ENCRYPTED_TEXT = "This data has been encrypted"; + const ENCRYPTED_KEY = "Re-encrypted Cipher Key"; + const mockEncryptedText = new EncString(ENCRYPTED_TEXT); + const mockEncryptedKey = new EncString(ENCRYPTED_KEY); + + // Changing mock data let mockCipherViews: CipherView[]; let mockMemberDetails: MemberCipherDetailsResponse[]; + let mockReport: ApplicationHealthReportDetail[]; + let mockSummary: OrganizationReportSummary; + let mockEncryptedReport: EncryptedDataWithKey; beforeEach(() => { cipherService.getAllFromApiForOrganization.mockResolvedValue(mockCiphers); @@ -115,6 +136,27 @@ describe("RiskInsightsReportService", () => { email: "user3@other.com", }), ]; + + mockReport = [ + { + applicationName: "app1", + passwordCount: 0, + atRiskPasswordCount: 0, + atRiskCipherIds: [], + memberCount: 0, + atRiskMemberCount: 0, + memberDetails: [], + atRiskMemberDetails: [], + cipherIds: [], + }, + ]; + mockSummary = createNewSummaryData(); + + mockEncryptedReport = { + organizationId: mockOrganizationId, + encryptedData: mockEncryptedText, + contentEncryptionKey: mockEncryptedKey, + }; }); it("should group and aggregate application health reports correctly", (done) => { @@ -137,7 +179,7 @@ describe("RiskInsightsReportService", () => { }); it("should generate the raw data report correctly", async () => { - const result = await firstValueFrom(service.LEGACY_generateRawDataReport$(mockOrgId)); + const result = await firstValueFrom(service.LEGACY_generateRawDataReport$(mockOrganizationId)); expect(result).toHaveLength(6); @@ -163,7 +205,7 @@ describe("RiskInsightsReportService", () => { }); it("should generate the raw data + uri report correctly", async () => { - const result = await firstValueFrom(service.generateRawDataUriReport$(mockOrgId)); + const result = await firstValueFrom(service.generateRawDataUriReport$(mockOrganizationId)); expect(result).toHaveLength(11); @@ -186,7 +228,9 @@ describe("RiskInsightsReportService", () => { }); it("should generate applications health report data correctly", async () => { - const result = await firstValueFrom(service.LEGACY_generateApplicationsReport$(mockOrgId)); + const result = await firstValueFrom( + service.LEGACY_generateApplicationsReport$(mockOrganizationId), + ); expect(result).toHaveLength(8); @@ -228,7 +272,7 @@ describe("RiskInsightsReportService", () => { it("should generate applications summary data correctly", async () => { const reportResult = await firstValueFrom( - service.LEGACY_generateApplicationsReport$(mockOrgId), + service.LEGACY_generateApplicationsReport$(mockOrganizationId), ); const reportSummary = service.generateApplicationsSummary(reportResult); @@ -238,56 +282,81 @@ describe("RiskInsightsReportService", () => { expect(reportSummary.totalAtRiskApplicationCount).toEqual(7); }); - describe("saveRiskInsightsReport", () => { - it("should not update subjects if save response does not have id", async () => { - const organizationId = "orgId" as OrganizationId; - const userId = "userId" as UserId; - const report = [{ applicationName: "app1" }] as any; - - const encryptedReport = { - organizationId: organizationId as OrganizationId, - encryptedData: "encryptedData" as EncryptedString, - encryptionKey: "encryptionKey" as EncryptedString, - }; - + describe("saveRiskInsightsReport$", () => { + it("should not update subjects if save response does not have id", (done) => { mockRiskInsightsEncryptionService.encryptRiskInsightsReport.mockResolvedValue( - encryptedReport, + mockEncryptedReport, ); - const saveResponse = { id: "" }; // Simulating no ID in response + const saveResponse = new SaveRiskInsightsReportResponse({ id: "" }); // Simulating no ID in response mockRiskInsightsApiService.saveRiskInsightsReport$.mockReturnValue(of(saveResponse)); - const reportSubjectSpy = jest.spyOn((service as any).riskInsightsReportSubject, "next"); - const summarySubjectSpy = jest.spyOn((service as any).riskInsightsSummarySubject, "next"); - - await service.saveRiskInsightsReport(organizationId, userId, report); - - expect(reportSubjectSpy).not.toHaveBeenCalled(); - expect(summarySubjectSpy).not.toHaveBeenCalled(); + service + .saveRiskInsightsReport$(mockReport, mockSummary, { + organizationId: mockOrganizationId, + userId: mockUserId, + }) + .subscribe({ + next: (response) => { + done.fail("Expected error due to invalid response"); + }, + error: (error: unknown) => { + if (error instanceof ErrorResponse && error.statusCode) { + expect(error.message).toBe("Invalid response from API"); + } + done(); + }, + }); }); + + it("should encrypt and save report, then update subjects", async () => {}); }); - describe("getRiskInsightsReport", () => { + describe("getRiskInsightsReport$", () => { beforeEach(() => { // Reset the mocks before each test jest.clearAllMocks(); }); - it("should call riskInsightsApiService.getRiskInsightsReport with the correct organizationId", () => { + it("should call with the correct organizationId", async () => { // we need to ensure that the api is invoked with the specified organizationId // here it doesn't matter what the Api returns const apiResponse = { id: "reportId", date: new Date().toISOString(), - organizationId: "orgId", - reportData: "encryptedReportData", - contentEncryptionKey: "encryptionKey", + organizationId: mockOrganizationId, + reportData: mockEncryptedReport.encryptedData, + contentEncryptionKey: mockEncryptedReport.contentEncryptionKey, } as GetRiskInsightsReportResponse; + const decryptedResponse: RiskInsightsReportData = { + data: [], + summary: { + totalMemberCount: 1, + totalAtRiskMemberCount: 1, + totalApplicationCount: 1, + totalAtRiskApplicationCount: 1, + totalCriticalMemberCount: 1, + totalCriticalAtRiskMemberCount: 1, + totalCriticalApplicationCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: [], + }, + }; + const organizationId = "orgId" as OrganizationId; const userId = "userId" as UserId; + + // Mock api returned encrypted data mockRiskInsightsApiService.getRiskInsightsReport$.mockReturnValue(of(apiResponse)); - service.getRiskInsightsReport(organizationId, userId); + + // Mock decrypted data + mockRiskInsightsEncryptionService.decryptRiskInsightsReport.mockReturnValue( + Promise.resolve(decryptedResponse), + ); + + await firstValueFrom(service.getRiskInsightsReport$(organizationId, userId)); + expect(mockRiskInsightsApiService.getRiskInsightsReport$).toHaveBeenCalledWith( organizationId, ); @@ -310,8 +379,8 @@ describe("RiskInsightsReportService", () => { id: "reportId", date: new Date().toISOString(), organizationId: organizationId as OrganizationId, - reportData: "encryptedReportData", - contentEncryptionKey: "encryptionKey", + reportData: mockEncryptedReport.encryptedData, + contentEncryptionKey: mockEncryptedReport.contentEncryptionKey, } as GetRiskInsightsReportResponse; const decryptedReport = { @@ -322,12 +391,7 @@ describe("RiskInsightsReportService", () => { decryptedReport, ); - const reportSubjectSpy = jest.spyOn((service as any).riskInsightsReportSubject, "next"); - - service.getRiskInsightsReport(organizationId, userId); - - // Wait for all microtasks to complete - await Promise.resolve(); + const result = await firstValueFrom(service.getRiskInsightsReport$(organizationId, userId)); expect(mockRiskInsightsEncryptionService.decryptRiskInsightsReport).toHaveBeenCalledWith( organizationId, @@ -336,7 +400,7 @@ describe("RiskInsightsReportService", () => { expect.anything(), expect.any(Function), ); - expect(reportSubjectSpy).toHaveBeenCalledWith(decryptedReport.data); + expect(result).toEqual(decryptedReport); }); }); }); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts index 1839e89a1ae..d82366c0154 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe -// @ts-strict-ignore import { BehaviorSubject, concatMap, @@ -11,15 +9,17 @@ import { Observable, of, switchMap, + throwError, zip, } from "rxjs"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { + createNewReportData, + createNewSummaryData, flattenMemberDetails, getApplicationReportDetail, getFlattenedCipherDetails, @@ -27,6 +27,10 @@ import { getTrimmedCipherUris, getUniqueMembers, } from "../helpers/risk-insights-data-mappers"; +import { + isSaveRiskInsightsReportResponse, + SaveRiskInsightsReportResponse, +} from "../models/api-models.types"; import { LEGACY_CipherHealthReportDetail, LEGACY_CipherHealthReportUriDetail, @@ -53,17 +57,9 @@ export class RiskInsightsReportService { private riskInsightsReportSubject = new BehaviorSubject([]); riskInsightsReport$ = this.riskInsightsReportSubject.asObservable(); - private riskInsightsSummarySubject = new BehaviorSubject({ - totalMemberCount: 0, - totalAtRiskMemberCount: 0, - totalApplicationCount: 0, - totalAtRiskApplicationCount: 0, - totalCriticalMemberCount: 0, - totalCriticalAtRiskMemberCount: 0, - totalCriticalApplicationCount: 0, - totalCriticalAtRiskApplicationCount: 0, - newApplications: [], - }); + private riskInsightsSummarySubject = new BehaviorSubject( + createNewSummaryData(), + ); riskInsightsSummary$ = this.riskInsightsSummarySubject.asObservable(); // [FIXME] CipherData @@ -189,11 +185,8 @@ export class RiskInsightsReportService { cipherHealthReportDetails.forEach((app) => { app.atRiskMemberDetails.forEach((member) => { - if (memberRiskMap.has(member.email)) { - memberRiskMap.set(member.email, memberRiskMap.get(member.email) + 1); - } else { - memberRiskMap.set(member.email, 1); - } + const currentCount = memberRiskMap.get(member.email) ?? 0; + memberRiskMap.set(member.email, currentCount + 1); }); }); @@ -206,25 +199,24 @@ export class RiskInsightsReportService { generateAtRiskApplicationList( cipherHealthReportDetails: ApplicationHealthReportDetail[], ): AtRiskApplicationDetail[] { - const appsRiskMap = new Map(); + const applicationPasswordRiskMap = new Map(); cipherHealthReportDetails .filter((app) => app.atRiskPasswordCount > 0) .forEach((app) => { - if (appsRiskMap.has(app.applicationName)) { - appsRiskMap.set( - app.applicationName, - appsRiskMap.get(app.applicationName) + app.atRiskPasswordCount, - ); - } else { - appsRiskMap.set(app.applicationName, app.atRiskPasswordCount); - } + const atRiskPasswordCount = applicationPasswordRiskMap.get(app.applicationName) ?? 0; + applicationPasswordRiskMap.set( + app.applicationName, + atRiskPasswordCount + app.atRiskPasswordCount, + ); }); - return Array.from(appsRiskMap.entries()).map(([applicationName, atRiskPasswordCount]) => ({ - applicationName, - atRiskPasswordCount, - })); + return Array.from(applicationPasswordRiskMap.entries()).map( + ([applicationName, atRiskPasswordCount]) => ({ + applicationName, + atRiskPasswordCount, + }), + ); } /** @@ -270,78 +262,88 @@ export class RiskInsightsReportService { return dataWithCiphers; } - getRiskInsightsReport(organizationId: OrganizationId, userId: UserId): void { - this.riskInsightsApiService - .getRiskInsightsReport$(organizationId) - .pipe( - switchMap((response) => { - if (!response) { - // Return an empty report and summary if response is falsy - return of({ - data: [], - summary: { - totalMemberCount: 0, - totalAtRiskMemberCount: 0, - totalApplicationCount: 0, - totalAtRiskApplicationCount: 0, - totalCriticalMemberCount: 0, - totalCriticalAtRiskMemberCount: 0, - totalCriticalApplicationCount: 0, - totalCriticalAtRiskApplicationCount: 0, - newApplications: [], - }, - }); - } - return from( - this.riskInsightsEncryptionService.decryptRiskInsightsReport( - organizationId, - userId, - new EncString(response.reportData), - new EncString(response.contentEncryptionKey), - (data) => data as RiskInsightsReportData, - ), - ); - }), - ) - .subscribe({ - next: (decryptRiskInsightsReport) => { - this.riskInsightsReportSubject.next(decryptRiskInsightsReport.data); - this.riskInsightsSummarySubject.next(decryptRiskInsightsReport.summary); - }, - }); - } - - async saveRiskInsightsReport( + /** + * Gets the risk insights report for a specific organization and user. + * + * @param organizationId + * @param userId + * @returns An observable that emits the decrypted risk insights report data. + */ + getRiskInsightsReport$( organizationId: OrganizationId, userId: UserId, + ): Observable { + return this.riskInsightsApiService.getRiskInsightsReport$(organizationId).pipe( + switchMap((response): Observable => { + if (!response) { + // Return an empty report and summary if response is falsy + return of(createNewReportData()); + } + if (!response.contentEncryptionKey || response.contentEncryptionKey.data == "") { + return throwError(() => new Error("Report key not found")); + } + if (!response.reportData) { + return throwError(() => new Error("Report data not found")); + } + return from( + this.riskInsightsEncryptionService.decryptRiskInsightsReport( + organizationId, + userId, + response.reportData, + response.contentEncryptionKey, + (data) => data as RiskInsightsReportData, + ), + ).pipe(map((decryptedReport) => decryptedReport ?? createNewReportData())); + }), + ); + } + + /** + * Encrypts the risk insights report data for a specific organization. + * @param organizationId The ID of the organization. + * @param userId The ID of the user. + * @param report The report data to encrypt. + * @returns A promise that resolves to an object containing the encrypted data and encryption key. + */ + saveRiskInsightsReport$( report: ApplicationHealthReportDetail[], - ): Promise { - const riskReport = { - data: report, - }; - - const encryptedReport = await this.riskInsightsEncryptionService.encryptRiskInsightsReport( - organizationId, - userId, - riskReport, + summary: OrganizationReportSummary, + encryptionParameters: { + organizationId: OrganizationId; + userId: UserId; + }, + ): Observable { + return from( + this.riskInsightsEncryptionService.encryptRiskInsightsReport( + encryptionParameters.organizationId, + encryptionParameters.userId, + { + data: report, + summary: summary, + }, + ), + ).pipe( + map(({ encryptedData, contentEncryptionKey }) => ({ + data: { + organizationId: encryptionParameters.organizationId, + date: new Date().toISOString(), + reportData: encryptedData.toSdk(), + contentEncryptionKey: contentEncryptionKey.toSdk(), + }, + })), + switchMap((encryptedReport) => + this.riskInsightsApiService.saveRiskInsightsReport$( + encryptedReport, + encryptionParameters.organizationId, + ), + ), + map((response) => { + if (!isSaveRiskInsightsReportResponse(response)) { + throw new Error("Invalid response from API"); + } + return response; + }), ); - - const saveRequest = { - data: { - organizationId: organizationId, - date: new Date().toISOString(), - reportData: encryptedReport.encryptedData, - reportKey: encryptedReport.encryptionKey, - }, - }; - - const response = await firstValueFrom( - this.riskInsightsApiService.saveRiskInsightsReport$(saveRequest, organizationId), - ); - - if (response && response.id) { - this.riskInsightsReportSubject.next(report); - } } /** @@ -374,7 +376,7 @@ export class RiskInsightsReportService { passwordUseMap.set(cipher.login.password, 1); } - const exposedPassword = exposedDetails.find((x) => x.cipherId === cipher.id); + const exposedPassword = exposedDetails.find((x) => x?.cipherId === cipher.id); // Get the cipher members const cipherMembers = memberDetails.filter((x) => x.cipherId === cipher.id); @@ -599,7 +601,7 @@ export class RiskInsightsReportService { return this.passwordHealthService.auditPasswordLeaks$(validCiphers).pipe( map((exposedDetails) => { return validCiphers.map((cipher) => { - const exposedPassword = exposedDetails.find((x) => x.cipherId === cipher.id); + const exposedPassword = exposedDetails.find((x) => x?.cipherId === cipher.id); const cipherMembers = memberDetails.filter((x) => x.cipherId === cipher.id); const result = { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts index 51efafe501d..390d1f8f9d5 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts @@ -10,6 +10,7 @@ import { RiskInsightsDataService, RiskInsightsReportService, } from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers"; import { LEGACY_ApplicationHealthReportDetailWithCriticalFlag, LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, @@ -66,17 +67,7 @@ export class AllApplicationsComponent implements OnInit { protected organization = new Organization(); noItemsIcon = Security; protected markingAsCritical = false; - protected applicationSummary: OrganizationReportSummary = { - totalMemberCount: 0, - totalAtRiskMemberCount: 0, - totalApplicationCount: 0, - totalAtRiskApplicationCount: 0, - totalCriticalMemberCount: 0, - totalCriticalAtRiskMemberCount: 0, - totalCriticalApplicationCount: 0, - totalCriticalAtRiskApplicationCount: 0, - newApplications: [], - }; + protected applicationSummary: OrganizationReportSummary = createNewSummaryData(); destroyRef = inject(DestroyRef); isLoading$: Observable = of(false); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index a34cae44f14..208ba59fb9d 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -146,6 +146,9 @@ export class RiskInsightsComponent implements OnInit { this._isDrawerOpen = details.open; }); } + runReport = () => { + this.dataService.triggerReport(); + }; /** * Refreshes the data by re-fetching the applications report. From 0bfc5daa7c59cb39e965a2752a4e9e9b5f01d12a Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 29 Sep 2025 15:19:14 -0500 Subject: [PATCH 33/55] [PM-26074] All Activities tab - Updated UI (#16587) * PM-26074 simplified and update the all-activities tab * PM-26074 removed learn more * PM-26074 fixing missing line --- apps/web/src/locales/en/messages.json | 30 +++++++++++- .../services/all-activities.service.ts | 44 ++++++++++++++++++ .../reports/risk-insights/services/index.ts | 1 + .../access-intelligence.module.ts | 6 +++ .../activity-card.component.html | 17 +++++-- .../activity-card.component.ts | 30 +++++++++--- .../all-activity.component.html | 30 ++++++++---- .../all-activity.component.ts | 46 +++++++++---------- .../all-applications.component.ts | 3 ++ .../critical-applications.component.ts | 3 ++ 10 files changed, 168 insertions(+), 42 deletions(-) create mode 100644 bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b4ad8f2b2b2..1b374c97478 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -59,9 +59,28 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "onceYouMarkCriticalApplicationsActivityDescription": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +99,15 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts new file mode 100644 index 00000000000..f1eebf81d73 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts @@ -0,0 +1,44 @@ +import { BehaviorSubject } from "rxjs"; + +import { OrganizationReportSummary } from "../models/report-models"; + +export class AllActivitiesService { + /// This class is used to manage the summary of all applications + /// and critical applications. + /// Going forward, this class can be simplified by using the RiskInsightsDataService + /// as it contains the application summary data. + + private reportSummarySubject$ = new BehaviorSubject({ + totalMemberCount: 0, + totalCriticalMemberCount: 0, + totalAtRiskMemberCount: 0, + totalCriticalAtRiskMemberCount: 0, + totalApplicationCount: 0, + totalCriticalApplicationCount: 0, + totalAtRiskApplicationCount: 0, + totalCriticalAtRiskApplicationCount: 0, + newApplications: [], + }); + + reportSummary$ = this.reportSummarySubject$.asObservable(); + + setCriticalAppsReportSummary(summary: OrganizationReportSummary) { + this.reportSummarySubject$.next({ + ...this.reportSummarySubject$.getValue(), + totalCriticalApplicationCount: summary.totalApplicationCount, + totalCriticalAtRiskApplicationCount: summary.totalAtRiskApplicationCount, + totalCriticalMemberCount: summary.totalMemberCount, + totalCriticalAtRiskMemberCount: summary.totalAtRiskMemberCount, + }); + } + + setAllAppsReportSummary(summary: OrganizationReportSummary) { + this.reportSummarySubject$.next({ + ...this.reportSummarySubject$.getValue(), + totalMemberCount: summary.totalMemberCount, + totalAtRiskMemberCount: summary.totalAtRiskMemberCount, + totalApplicationCount: summary.totalApplicationCount, + totalAtRiskApplicationCount: summary.totalAtRiskApplicationCount, + }); + } +} diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts index e3f75ea0daf..69d936d3016 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts @@ -5,3 +5,4 @@ export * from "./critical-apps-api.service"; export * from "./risk-insights-api.service"; export * from "./risk-insights-report.service"; export * from "./risk-insights-data.service"; +export * from "./all-activities.service"; diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts index 1d80f2154b1..6848220446b 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts @@ -3,6 +3,7 @@ import { NgModule } from "@angular/core"; import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { CriticalAppsService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { + AllActivitiesService, CriticalAppsApiService, MemberCipherDetailsApiService, PasswordHealthService, @@ -73,6 +74,11 @@ import { RiskInsightsComponent } from "./risk-insights.component"; useClass: CriticalAppsApiService, deps: [ApiService], }), + safeProvider({ + provide: AllActivitiesService, + useClass: AllActivitiesService, + deps: [], + }), ], }) export class AccessIntelligenceModule {} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.html index 227ca555786..17ae964dbed 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.html @@ -1,9 +1,18 @@
- {{ title | i18n }} + {{ title }}
- {{ cardMetrics | i18n: value }} + {{ cardMetrics }}
-
- {{ metricDescription | i18n }} +
+ {{ metricDescription }}
+ @if (showNavigationLink) { + + }
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.ts index 5b9433634e6..7de339358f3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.ts @@ -1,13 +1,14 @@ import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; +import { Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { TypographyModule } from "@bitwarden/components"; +import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components"; @Component({ selector: "dirt-activity-card", templateUrl: "./activity-card.component.html", - imports: [CommonModule, TypographyModule, JslibModule], + imports: [CommonModule, TypographyModule, JslibModule, LinkModule, ButtonModule], host: { class: "tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-rounded-lg tw-p-6", @@ -18,10 +19,6 @@ export class ActivityCardComponent { * The title of the card goes here */ @Input() title: string = ""; - /** - * The current value of the card as emphasized text - */ - @Input() value: number | null = null; /** * The card metrics text to display next to the value */ @@ -30,4 +27,25 @@ export class ActivityCardComponent { * The description text to display below the value and metrics */ @Input() metricDescription: string = ""; + + /** + * The link to navigate to for more information + */ + @Input() navigationLink: string = ""; + + /** + * The text to display for the navigation link + */ + @Input() navigationText: string = ""; + + /** + * Show Navigation link + */ + @Input() showNavigationLink: boolean = false; + + constructor(private router: Router) {} + + navigateToLink = async (navigationLink: string) => { + await this.router.navigateByUrl(navigationLink); + }; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html index 35c9fb451e6..6598d197172 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html @@ -21,19 +21,33 @@
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.ts index da78f385cbd..e69dc2b06e5 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.ts @@ -1,23 +1,22 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute } from "@angular/router"; -import { BehaviorSubject, combineLatest, firstValueFrom, of, switchMap } from "rxjs"; +import { BehaviorSubject, firstValueFrom } from "rxjs"; import { - CriticalAppsService, + AllActivitiesService, RiskInsightsDataService, - RiskInsightsReportService, } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getById } from "@bitwarden/common/platform/misc"; -import { OrganizationId } from "@bitwarden/common/types/guid"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { ActivityCardComponent } from "./activity-card.component"; import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"; +import { RiskInsightsTabType } from "./risk-insights.component"; @Component({ selector: "tools-all-activity", @@ -28,8 +27,9 @@ export class AllActivityComponent implements OnInit { protected isLoading$ = this.dataService.isLoading$; protected noData$ = new BehaviorSubject(true); organization: Organization | null = null; - atRiskMemberCount = 0; - criticalApplicationsCount = 0; + totalCriticalAppsAtRiskMemberCount = 0; + totalCriticalAppsCount = 0; + totalCriticalAppsAtRiskCount = 0; destroyRef = inject(DestroyRef); @@ -43,21 +43,13 @@ export class AllActivityComponent implements OnInit { this.organizationService.organizations$(userId).pipe(getById(organizationId)), )) ?? null; - combineLatest([ - this.dataService.applications$, - this.criticalAppsService.getAppsListForOrg(organizationId as OrganizationId), - ]) - .pipe( - takeUntilDestroyed(this.destroyRef), - switchMap(([apps, criticalApps]) => { - const atRiskMembers = this.reportService.generateAtRiskMemberList(apps ?? []); - return of({ apps, atRiskMembers, criticalApps }); - }), - ) - .subscribe(({ apps, atRiskMembers, criticalApps }) => { - this.noData$.next((apps?.length ?? 0) === 0); - this.atRiskMemberCount = atRiskMembers?.length ?? 0; - this.criticalApplicationsCount = criticalApps?.length ?? 0; + this.allActivitiesService.reportSummary$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((summary) => { + this.noData$.next(summary.totalApplicationCount === 0); + this.totalCriticalAppsAtRiskMemberCount = summary.totalCriticalAtRiskMemberCount; + this.totalCriticalAppsCount = summary.totalCriticalApplicationCount; + this.totalCriticalAppsAtRiskCount = summary.totalCriticalAtRiskApplicationCount; }); } } @@ -67,7 +59,15 @@ export class AllActivityComponent implements OnInit { private accountService: AccountService, protected organizationService: OrganizationService, protected dataService: RiskInsightsDataService, - protected reportService: RiskInsightsReportService, - protected criticalAppsService: CriticalAppsService, + protected allActivitiesService: AllActivitiesService, ) {} + + get RiskInsightsTabType() { + return RiskInsightsTabType; + } + + getLinkForRiskInsightsTab(tabIndex: RiskInsightsTabType): string { + const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId"); + return `/organizations/${organizationId}/access-intelligence/risk-insights?tabIndex=${tabIndex}`; + } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts index 390d1f8f9d5..3b7490dbc19 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts @@ -6,6 +6,7 @@ import { combineLatest, debounceTime, firstValueFrom, map, Observable, of, switc import { Security } from "@bitwarden/assets/svg"; import { + AllActivitiesService, CriticalAppsService, RiskInsightsDataService, RiskInsightsReportService, @@ -120,6 +121,7 @@ export class AllApplicationsComponent implements OnInit { if (data) { this.dataSource.data = data; this.applicationSummary = this.reportService.generateApplicationsSummary(data); + this.allActivitiesService.setAllAppsReportSummary(this.applicationSummary); } if (organization) { this.organization = organization; @@ -142,6 +144,7 @@ export class AllApplicationsComponent implements OnInit { private accountService: AccountService, protected criticalAppsService: CriticalAppsService, protected riskInsightsEncryptionService: RiskInsightsEncryptionService, + protected allActivitiesService: AllActivitiesService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts index 9110779c980..f092b1575f0 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts @@ -8,6 +8,7 @@ import { combineLatest, debounceTime, firstValueFrom, map, switchMap } from "rxj import { Security } from "@bitwarden/assets/svg"; import { + AllActivitiesService, CriticalAppsService, RiskInsightsDataService, RiskInsightsReportService, @@ -99,6 +100,7 @@ export class CriticalApplicationsComponent implements OnInit { this.dataSource.data = applications; this.applicationSummary = this.reportService.generateApplicationsSummary(applications); this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0; + this.allActivitiesService.setCriticalAppsReportSummary(this.applicationSummary); } }); } @@ -176,6 +178,7 @@ export class CriticalApplicationsComponent implements OnInit { private configService: ConfigService, private adminTaskService: DefaultAdminTaskService, private accountService: AccountService, + private allActivitiesService: AllActivitiesService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) From 5f7e1f99bf5826026181443c00e123b919f10968 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:29:56 -0700 Subject: [PATCH 34/55] chore(flag-removal): [Auth/PM20439] Remove flagging logic and flag (BrowserExtensionLoginApproval) (#16568) --- .../settings/account-security.component.html | 2 +- .../account-security.component.spec.ts | 4 +- .../settings/account-security.component.ts | 8 -- apps/browser/src/popup/app-routing.module.ts | 4 +- apps/browser/src/popup/app.component.ts | 81 ++++++++----------- .../src/vault/app/vault/vault-v2.component.ts | 28 ++----- .../services/vault-banners.service.spec.ts | 11 +-- .../services/vault-banners.service.ts | 25 +----- libs/common/src/enums/feature-flag.enum.ts | 2 - ...ult-server-notifications.multiuser.spec.ts | 1 - .../default-server-notifications.service.ts | 25 +++--- 11 files changed, 64 insertions(+), 127 deletions(-) diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html index 3de1cc81a69..44900acc065 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -102,7 +102,7 @@ - +

{{ "manageDevices" | i18n }}

diff --git a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts index 63666440a76..2335c5c2e69 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts @@ -1,6 +1,7 @@ import { Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; +import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; @@ -19,7 +20,6 @@ import { VaultTimeoutStringType, VaultTimeoutAction, } from "@bitwarden/common/key-management/vault-timeout"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -67,6 +67,7 @@ describe("AccountSecurityComponent", () => { providers: [ { provide: AccountService, useValue: accountService }, { provide: AccountSecurityComponent, useValue: mock() }, + { provide: ActivatedRoute, useValue: mock() }, { provide: BiometricsService, useValue: mock() }, { provide: BiometricStateService, useValue: biometricStateService }, { provide: DialogService, useValue: dialogService }, @@ -88,7 +89,6 @@ describe("AccountSecurityComponent", () => { { provide: LogService, useValue: mock() }, { provide: OrganizationService, useValue: mock() }, { provide: CollectionService, useValue: mock() }, - { provide: ConfigService, useValue: mock() }, { provide: ValidationService, useValue: validationService }, ], }) diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 72a389ecf71..0c9b4634569 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -31,7 +31,6 @@ import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/ import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { VaultTimeout, @@ -41,7 +40,6 @@ import { VaultTimeoutSettingsService, VaultTimeoutStringType, } from "@bitwarden/common/key-management/vault-timeout"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -115,7 +113,6 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { biometricUnavailabilityReason: string; showChangeMasterPass = true; pinEnabled$: Observable = of(true); - extensionLoginApprovalFlagEnabled = false; form = this.formBuilder.group({ vaultTimeout: [null as VaultTimeout | null], @@ -157,7 +154,6 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private biometricsService: BiometricsService, private vaultNudgesService: NudgesService, private validationService: ValidationService, - private configService: ConfigService, private logService: LogService, ) {} @@ -239,10 +235,6 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { }; this.form.patchValue(initialValues, { emitEvent: false }); - this.extensionLoginApprovalFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM14938_BrowserExtensionLoginApproval, - ); - timer(0, 1000) .pipe( switchMap(async () => { diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 501ef0ba8ff..b69d7b73672 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -14,7 +14,6 @@ import { } from "@bitwarden/angular/auth/guards"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; -import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { DevicesIcon, RegistrationUserAddIcon, @@ -40,7 +39,6 @@ import { TwoFactorAuthComponent, TwoFactorAuthGuard, } from "@bitwarden/auth/angular"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui"; @@ -262,7 +260,7 @@ const routes: Routes = [ { path: "device-management", component: ExtensionDeviceManagementComponent, - canActivate: [canAccessFeature(FeatureFlag.PM14938_BrowserExtensionLoginApproval), authGuard], + canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, { diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 39f82622b68..998531488d3 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -42,9 +42,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -82,7 +80,6 @@ export class AppComponent implements OnInit, OnDestroy { private activeUserId: UserId; private routerAnimations = false; private processingPendingAuth = false; - private extensionLoginApprovalFeatureFlag = false; private destroy$ = new Subject(); @@ -118,7 +115,6 @@ export class AppComponent implements OnInit, OnDestroy { private authRequestService: AuthRequestServiceAbstraction, private pendingAuthRequestsState: PendingAuthRequestsStateService, private authRequestAnsweringService: AuthRequestAnsweringServiceAbstraction, - private readonly configService: ConfigService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); @@ -127,10 +123,6 @@ export class AppComponent implements OnInit, OnDestroy { } async ngOnInit() { - this.extensionLoginApprovalFeatureFlag = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval), - ); - initPopupClosedListener(); this.compactModeService.init(); @@ -140,24 +132,22 @@ export class AppComponent implements OnInit, OnDestroy { this.activeUserId = account?.id; }); - if (this.extensionLoginApprovalFeatureFlag) { - // Trigger processing auth requests when the active user is in an unlocked state. Runs once when - // the popup is open. - this.accountService.activeAccount$ - .pipe( - map((a) => a?.id), // Extract active userId - distinctUntilChanged(), // Only when userId actually changes - filter((userId) => userId != null), // Require a valid userId - switchMap((userId) => this.authService.authStatusFor$(userId).pipe(take(1))), // Get current auth status once for new user - filter((status) => status === AuthenticationStatus.Unlocked), // Only when the new user is Unlocked - tap(() => { - // Trigger processing when switching users while popup is open - void this.authRequestAnsweringService.processPendingAuthRequests(); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - } + // Trigger processing auth requests when the active user is in an unlocked state. Runs once when + // the popup is open. + this.accountService.activeAccount$ + .pipe( + map((a) => a?.id), // Extract active userId + distinctUntilChanged(), // Only when userId actually changes + filter((userId) => userId != null), // Require a valid userId + switchMap((userId) => this.authService.authStatusFor$(userId).pipe(take(1))), // Get current auth status once for new user + filter((status) => status === AuthenticationStatus.Unlocked), // Only when the new user is Unlocked + tap(() => { + // Trigger processing when switching users while popup is open + void this.authRequestAnsweringService.processPendingAuthRequests(); + }), + takeUntil(this.destroy$), + ) + .subscribe(); this.authService.activeAccountStatus$ .pipe( @@ -169,24 +159,22 @@ export class AppComponent implements OnInit, OnDestroy { ) .subscribe(); - if (this.extensionLoginApprovalFeatureFlag) { - // When the popup is already open and the active account transitions to Unlocked, - // process any pending auth requests for the active user. The above subscription does not handle - // this case. - this.authService.activeAccountStatus$ - .pipe( - startWith(null as unknown as AuthenticationStatus), // Seed previous value to handle initial emission - pairwise(), // Compare previous and current statuses - filter( - ([prev, curr]) => - prev !== AuthenticationStatus.Unlocked && curr === AuthenticationStatus.Unlocked, // Fire on transitions into Unlocked (incl. initial) - ), - takeUntil(this.destroy$), - ) - .subscribe(() => { - void this.authRequestAnsweringService.processPendingAuthRequests(); - }); - } + // When the popup is already open and the active account transitions to Unlocked, + // process any pending auth requests for the active user. The above subscription does not handle + // this case. + this.authService.activeAccountStatus$ + .pipe( + startWith(null as unknown as AuthenticationStatus), // Seed previous value to handle initial emission + pairwise(), // Compare previous and current statuses + filter( + ([prev, curr]) => + prev !== AuthenticationStatus.Unlocked && curr === AuthenticationStatus.Unlocked, // Fire on transitions into Unlocked (incl. initial) + ), + takeUntil(this.destroy$), + ) + .subscribe(() => { + void this.authRequestAnsweringService.processPendingAuthRequests(); + }); this.ngZone.runOutsideAngular(() => { window.onmousedown = () => this.recordActivity(); @@ -241,10 +229,7 @@ export class AppComponent implements OnInit, OnDestroy { } await this.router.navigate(["lock"]); - } else if ( - msg.command === "openLoginApproval" && - this.extensionLoginApprovalFeatureFlag - ) { + } else if (msg.command === "openLoginApproval") { if (this.processingPendingAuth) { return; } diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 5a6683ed904..3fdb14aa154 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -25,7 +25,6 @@ import { Account, AccountService } from "@bitwarden/common/auth/abstractions/acc import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -319,26 +318,13 @@ export class VaultV2Component this.searchBarService.setEnabled(true); this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); - if ( - (await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval), - )) === true - ) { - const authRequests = await firstValueFrom( - this.authRequestService.getLatestPendingAuthRequest$(), - ); - if (authRequests != null) { - this.messagingService.send("openLoginApproval", { - notificationId: authRequests.id, - }); - } - } else { - const authRequest = await this.apiService.getLastAuthRequest(); - if (authRequest != null) { - this.messagingService.send("openLoginApproval", { - notificationId: authRequest.id, - }); - } + const authRequests = await firstValueFrom( + this.authRequestService.getLatestPendingAuthRequest$(), + ); + if (authRequests != null) { + this.messagingService.send("openLoginApproval", { + notificationId: authRequests.id, + }); } this.activeUserId = await firstValueFrom( diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts index c97b23b1456..89a3757e939 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts @@ -1,6 +1,5 @@ import { TestBed } from "@angular/core/testing"; -import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs"; +import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs"; import { AuthRequestServiceAbstraction, @@ -13,7 +12,6 @@ import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/de import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DeviceType } from "@bitwarden/common/enums"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -45,14 +43,11 @@ describe("VaultBannersService", () => { }); const devices$ = new BehaviorSubject([]); const pendingAuthRequests$ = new BehaviorSubject>([]); - let configService: MockProxy; beforeEach(() => { lastSync$.next(new Date("2024-05-14")); isSelfHost.mockClear(); getEmailVerified.mockClear().mockResolvedValue(true); - configService = mock(); - configService.getFeatureFlag$.mockImplementation(() => of(true)); TestBed.configureTestingModule({ providers: [ @@ -99,10 +94,6 @@ describe("VaultBannersService", () => { provide: AuthRequestServiceAbstraction, useValue: { getPendingAuthRequests$: () => pendingAuthRequests$ }, }, - { - provide: ConfigService, - useValue: configService, - }, ], }); }); diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index dd50c832cc6..c4396940998 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -6,10 +6,7 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateProvider, @@ -70,9 +67,7 @@ export class VaultBannersService { private kdfConfigService: KdfConfigService, private syncService: SyncService, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - private devicesService: DevicesServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, - private configService: ConfigService, ) {} /** Returns true when the pending auth request banner should be shown */ @@ -80,24 +75,12 @@ export class VaultBannersService { const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes( VisibleVaultBanner.PendingAuthRequest, ); - // TODO: PM-20439 remove feature flag - const browserLoginApprovalFeatureFlag = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval), + + const pendingAuthRequests = await firstValueFrom( + this.authRequestService.getPendingAuthRequests$(), ); - if (browserLoginApprovalFeatureFlag === true) { - const pendingAuthRequests = await firstValueFrom( - this.authRequestService.getPendingAuthRequests$(), - ); - return pendingAuthRequests.length > 0 && !alreadyDismissed; - } else { - const devices = await firstValueFrom(this.devicesService.getDevices$()); - const hasPendingRequest = devices.some( - (device) => device.response?.devicePendingAuthRequest != null, - ); - - return hasPendingRequest && !alreadyDismissed; - } + return pendingAuthRequests.length > 0 && !alreadyDismissed; } shouldShowPremiumBanner$(userId: UserId): Observable { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 18134fee2c3..bd874f934f0 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -15,7 +15,6 @@ export enum FeatureFlag { CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors", /* Auth */ - PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals", PM22110_DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods", /* Autofill */ @@ -98,7 +97,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM22136_SdkCipherEncryption]: FALSE, /* Auth */ - [FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE, [FeatureFlag.PM22110_DisableAlternateLoginMethods]: FALSE, /* Billing */ diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts index 9bc47437b39..b4d47698e4d 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts @@ -132,7 +132,6 @@ describe("DefaultServerNotificationsService (multi-user)", () => { const flagValueByFlag: Partial> = { [FeatureFlag.InactiveUserServerNotification]: true, [FeatureFlag.PushNotificationsWhenLocked]: true, - [FeatureFlag.PM14938_BrowserExtensionLoginApproval]: true, }; return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any; }); diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts index e934dec185d..47af8f5e00c 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts @@ -278,16 +278,21 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification); break; case NotificationType.AuthRequest: - if ( - await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval), - ) - ) { - await this.authRequestAnsweringService.receivedPendingAuthRequest( - notification.payload.userId, - notification.payload.id, - ); - } + await this.authRequestAnsweringService.receivedPendingAuthRequest( + notification.payload.userId, + notification.payload.id, + ); + + /** + * This call is necessary for Desktop, which for the time being uses a noop for the + * authRequestAnsweringService.receivedPendingAuthRequest() call just above. Desktop + * will eventually use the new AuthRequestAnsweringService, at which point we can remove + * this second call. + * + * The Extension AppComponent has logic (see processingPendingAuth) that only allows one + * pending auth request to process at a time, so this second call will not cause any + * duplicate processing conflicts on Extension. + */ this.messagingService.send("openLoginApproval", { notificationId: notification.payload.id, }); From f92eddf7b9f7d3dd7661e132d6b343d5c575d79e Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Mon, 29 Sep 2025 16:32:18 -0400 Subject: [PATCH 35/55] only run BIT trigger workflow when Build Browser workflow completes successfully (#16572) --- .github/workflows/test-browser-interactions.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-browser-interactions.yml b/.github/workflows/test-browser-interactions.yml index c6427b2e0d8..3af1a1a8e9d 100644 --- a/.github/workflows/test-browser-interactions.yml +++ b/.github/workflows/test-browser-interactions.yml @@ -11,6 +11,7 @@ jobs: check-files: name: Check files runs-on: ubuntu-22.04 + if: ${{ github.event.workflow_run.conclusion == 'success' }} permissions: actions: write contents: read From cae01c6e0f28cb7bc3836e63df0b173b68ff988c Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 30 Sep 2025 03:52:04 +0200 Subject: [PATCH 36/55] Fix flood of Angular warning messages on policies page (#16618) Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .../policies/policies.component.html | 23 ++++------ .../policies/policies.component.ts | 46 +++++++++++++------ 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.html b/apps/web/src/app/admin-console/organizations/policies/policies.component.html index ea14986749f..8df73a50e14 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.html @@ -1,7 +1,6 @@ - @let organization = organization$ | async; @if (loading) { - @for (p of policies; track p.name) { - @if (p.display$(organization, configService) | async) { - - - - @if (policiesEnabledMap.get(p.type)) { - {{ "on" | i18n }} - } - {{ p.description | i18n }} - - - } + @for (p of policies$ | async; track p.type) { + + + + @if (policiesEnabledMap.get(p.type)) { + {{ "on" | i18n }} + } + {{ p.description | i18n }} + + } diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 45383133687..e2c51b77d45 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -2,8 +2,17 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, lastValueFrom, Observable } from "rxjs"; -import { first, map } from "rxjs/operators"; +import { + combineLatest, + firstValueFrom, + lastValueFrom, + Observable, + of, + switchMap, + first, + map, + withLatestFrom, +} from "rxjs"; import { getOrganizationById, @@ -11,7 +20,6 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -39,8 +47,7 @@ import { POLICY_EDIT_REGISTER } from "./policy-register-token"; export class PoliciesComponent implements OnInit { loading = true; organizationId: string; - policies: readonly BasePolicyEditDefinition[]; - protected organization$: Observable; + policies$: Observable; private orgPolicies: PolicyResponse[]; protected policiesEnabledMap: Map = new Map(); @@ -63,28 +70,41 @@ export class PoliciesComponent implements OnInit { this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - this.organization$ = this.organizationService + const organization$ = this.organizationService .organizations$(userId) .pipe(getOrganizationById(this.organizationId)); - this.policies = this.policyListService.getPolicies(); + this.policies$ = organization$.pipe( + withLatestFrom(of(this.policyListService.getPolicies())), + switchMap(([organization, policies]) => { + return combineLatest( + policies.map((policy) => + policy + .display$(organization, this.configService) + .pipe(map((shouldDisplay) => ({ policy, shouldDisplay }))), + ), + ); + }), + map((results) => + results.filter((result) => result.shouldDisplay).map((result) => result.policy), + ), + ); await this.load(); // Handle policies component launch from Event message - this.route.queryParams - .pipe(first()) + combineLatest([this.route.queryParams.pipe(first()), this.policies$]) /* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */ - .subscribe(async (qParams) => { + .subscribe(async ([qParams, policies]) => { if (qParams.policyId != null) { const policyIdFromEvents: string = qParams.policyId; for (const orgPolicy of this.orgPolicies) { if (orgPolicy.id === policyIdFromEvents) { - for (let i = 0; i < this.policies.length; i++) { - if (this.policies[i].type === orgPolicy.type) { + for (let i = 0; i < policies.length; i++) { + if (policies[i].type === orgPolicy.type) { // 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.edit(this.policies[i]); + this.edit(policies[i]); break; } } From 2ccd841f5807e7086b3b141a6435b3fed232d72e Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Tue, 30 Sep 2025 07:53:10 -0400 Subject: [PATCH 37/55] feat(Utils.fromBufferToB64): [Platform/PM-26186] Add type safety and ArrayBufferView support + tests (#16609) * PM-26186 - Utils.ts - fromBufferToB64 - (1) Add type safety (2) Add ArrayBufferView support (3) Add tests * PM-26186 - Utils.ts - add overloads so that we can specify callers who pass defined buffers will always get a string back so I don't have to modify all call sites to add a null assertion or "as string" --- libs/common/src/platform/misc/utils.spec.ts | 77 ++++++++++++++++++++- libs/common/src/platform/misc/utils.ts | 67 +++++++++++++++++- 2 files changed, 140 insertions(+), 4 deletions(-) diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index 818138863fb..9f01db61fa6 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -302,7 +302,7 @@ describe("Utils Service", () => { expect(b64String).toBe(b64HelloWorldString); }); - runInBothEnvironments("should return an empty string for an empty ArrayBuffer", () => { + runInBothEnvironments("should return empty string for an empty ArrayBuffer", () => { const buffer = new Uint8Array([]).buffer; const b64String = Utils.fromBufferToB64(buffer); expect(b64String).toBe(""); @@ -312,6 +312,81 @@ describe("Utils Service", () => { const b64String = Utils.fromBufferToB64(null); expect(b64String).toBeNull(); }); + + runInBothEnvironments("returns null for undefined input", () => { + const b64 = Utils.fromBufferToB64(undefined as unknown as ArrayBuffer); + expect(b64).toBeNull(); + }); + + runInBothEnvironments("returns empty string for empty input", () => { + const b64 = Utils.fromBufferToB64(new ArrayBuffer(0)); + expect(b64).toBe(""); + }); + + runInBothEnvironments("accepts Uint8Array directly", () => { + const u8 = new Uint8Array(asciiHelloWorldArray); + const b64 = Utils.fromBufferToB64(u8); + expect(b64).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("respects byteOffset/byteLength (view window)", () => { + // [xx, 'hello world', yy] — view should only encode the middle slice + const prefix = [1, 2, 3]; + const suffix = [4, 5]; + const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]); + const view = new Uint8Array(all.buffer, prefix.length, asciiHelloWorldArray.length); + const b64 = Utils.fromBufferToB64(view); + expect(b64).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("handles DataView (ArrayBufferView other than Uint8Array)", () => { + const u8 = new Uint8Array(asciiHelloWorldArray); + const dv = new DataView(u8.buffer, 0, u8.byteLength); + const b64 = Utils.fromBufferToB64(dv); + expect(b64).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("handles DataView with offset/length window", () => { + // Buffer: [xx, 'hello world', yy] + const prefix = [9, 9, 9]; + const suffix = [8, 8]; + const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]); + + // DataView over just the "hello world" window + const dv = new DataView(all.buffer, prefix.length, asciiHelloWorldArray.length); + + const b64 = Utils.fromBufferToB64(dv); + expect(b64).toBe(b64HelloWorldString); + }); + + runInBothEnvironments( + "encodes empty view (offset-length window of zero) as empty string", + () => { + const backing = new Uint8Array([1, 2, 3, 4]); + const emptyView = new Uint8Array(backing.buffer, 2, 0); + const b64 = Utils.fromBufferToB64(emptyView); + expect(b64).toBe(""); + }, + ); + + runInBothEnvironments("does not mutate the input", () => { + const original = new Uint8Array(asciiHelloWorldArray); + const copyBefore = new Uint8Array(original); // snapshot + Utils.fromBufferToB64(original); + expect(original).toEqual(copyBefore); // unchanged + }); + + it("produces the same Base64 in Node vs non-Node mode", () => { + const bytes = new Uint8Array(asciiHelloWorldArray); + + Utils.isNode = true; + const nodeB64 = Utils.fromBufferToB64(bytes); + + Utils.isNode = false; + const browserB64 = Utils.fromBufferToB64(bytes); + + expect(browserB64).toBe(nodeB64); + }); }); describe("fromB64ToArray(...)", () => { diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index c103e346a85..43a9e43d92b 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -128,15 +128,52 @@ export class Utils { return arr; } - static fromBufferToB64(buffer: ArrayBuffer): string { + /** + * Convert binary data into a Base64 string. + * + * Overloads are provided for two categories of input: + * + * 1. ArrayBuffer + * - A raw, fixed-length chunk of memory (no element semantics). + * - Example: `const buf = new ArrayBuffer(16);` + * + * 2. ArrayBufferView + * - A *view* onto an existing buffer that gives the bytes meaning. + * - Examples: Uint8Array, Int32Array, DataView, etc. + * - Views can expose only a *window* of the underlying buffer via + * `byteOffset` and `byteLength`. + * Example: + * ```ts + * const buf = new ArrayBuffer(8); + * const full = new Uint8Array(buf); // sees all 8 bytes + * const half = new Uint8Array(buf, 4, 4); // sees only last 4 bytes + * ``` + * + * Returns: + * - Base64 string for non-empty inputs, + * - null if `buffer` is `null` or `undefined` + * - empty string if `buffer` is empty (0 bytes) + */ + static fromBufferToB64(buffer: null | undefined): null; + static fromBufferToB64(buffer: ArrayBuffer): string; + static fromBufferToB64(buffer: ArrayBufferView): string; + static fromBufferToB64(buffer: ArrayBuffer | ArrayBufferView | null | undefined): string | null { + // Handle null / undefined input if (buffer == null) { return null; } + + const bytes: Uint8Array = Utils.normalizeToUint8Array(buffer); + + // Handle empty input + if (bytes.length === 0) { + return ""; + } + if (Utils.isNode) { - return Buffer.from(buffer).toString("base64"); + return Buffer.from(bytes).toString("base64"); } else { let binary = ""; - const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } @@ -144,6 +181,30 @@ export class Utils { } } + /** + * Normalizes input into a Uint8Array so we always have a uniform, + * byte-level view of the data. This avoids dealing with differences + * between ArrayBuffer (raw memory with no indexing) and other typed + * views (which may have element sizes, offsets, and lengths). + * @param buffer ArrayBuffer or ArrayBufferView (e.g. Uint8Array, DataView, etc.) + */ + private static normalizeToUint8Array(buffer: ArrayBuffer | ArrayBufferView): Uint8Array { + /** + * 1) Uint8Array: already bytes → use directly. + * 2) ArrayBuffer: wrap whole buffer. + * 3) Other ArrayBufferView (e.g., DataView, Int32Array): + * wrap the view’s window (byteOffset..byteOffset+byteLength). + */ + if (buffer instanceof Uint8Array) { + return buffer; + } else if (buffer instanceof ArrayBuffer) { + return new Uint8Array(buffer); + } else { + const view = buffer as ArrayBufferView; + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); + } + } + static fromBufferToUrlB64(buffer: ArrayBuffer): string { return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer)); } From 54a53a1c348808c0dfeb917626887070d8b00c64 Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Tue, 30 Sep 2025 06:33:32 -0600 Subject: [PATCH 38/55] Use tracing in ssh_agent (#16455) * [BEEEP][PM-255518] Use tracing for improved observability * feedback dani-garcia: use DefaultVisitor * set default log level * convert printlns in objc crate * convert printlns in autotype crate * convert printlns in autostart crate * convert printlns in core/password crate * convert printlns in core/biometric crate * convert printlns in napi crate * convert log usage in macos provider crate * convert existing log macros to tracing * fix the cargo.toml sort lint errors * Revert "fix the cargo.toml sort lint errors" This reverts commit fd149ab697d37ea8fc9c22db8f96684fc99bf2d8. * fix the sort lint using correct cargo sort version * feedback coltonhurst: more comments/clarity on behavior * revert changes to ssh_agent * Use tracing in ssh_agent --- .../desktop_native/core/src/ssh_agent/mod.rs | 23 ++++++++-------- .../ssh_agent/named_pipe_listener_stream.rs | 27 +++++++++---------- .../desktop_native/core/src/ssh_agent/unix.rs | 17 ++++++------ 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs index d038ba2277f..3440a0114ae 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -11,6 +11,7 @@ use bitwarden_russh::{ session_bind::SessionBindResult, ssh_agent::{self, SshKey}, }; +use tracing::{error, info}; #[cfg_attr(target_os = "windows", path = "windows.rs")] #[cfg_attr(target_os = "macos", path = "unix.rs")] @@ -86,7 +87,7 @@ impl ssh_agent::Agent info: &peerinfo::models::PeerInfo, ) -> bool { if !self.is_running() { - println!("[BitwardenDesktopAgent] Agent is not running, but tried to call confirm"); + error!("Agent is not running, but tried to call confirm"); return false; } @@ -94,7 +95,7 @@ impl ssh_agent::Agent let request_data = match request_parser::parse_request(data) { Ok(data) => data, Err(e) => { - println!("[SSH Agent] Error while parsing request: {e}"); + error!(error = %e, "Error while parsing request"); return false; } }; @@ -105,12 +106,12 @@ impl ssh_agent::Agent _ => None, }; - println!( - "[SSH Agent] Confirming request from application: {}, is_forwarding: {}, namespace: {}, host_key: {}", + info!( + is_forwarding = %info.is_forwarding(), + namespace = ?namespace.as_ref(), + host_key = %STANDARD.encode(info.host_key()), + "Confirming request from application: {}", info.process_name(), - info.is_forwarding(), - namespace.clone().unwrap_or_default(), - STANDARD.encode(info.host_key()) ); let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe(); @@ -172,7 +173,7 @@ impl ssh_agent::Agent connection_info.set_host_key(session_bind_info.host_key.clone()); } SessionBindResult::SignatureFailure => { - println!("[BitwardenDesktopAgent] Session bind failure: Signature failure"); + error!("Session bind failure: Signature failure"); } } } @@ -181,7 +182,7 @@ impl ssh_agent::Agent impl BitwardenDesktopAgent { pub fn stop(&self) { if !self.is_running() { - println!("[BitwardenDesktopAgent] Tried to stop agent while it is not running"); + error!("Tried to stop agent while it is not running"); return; } @@ -227,7 +228,7 @@ impl BitwardenDesktopAgent { ); } Err(e) => { - eprintln!("[SSH Agent Native Module] Error while parsing key: {e}"); + error!(error=%e, "Error while parsing key"); } } } @@ -265,7 +266,7 @@ impl BitwardenDesktopAgent { fn get_request_id(&self) -> u32 { if !self.is_running() { - println!("[BitwardenDesktopAgent] Agent is not running, but tried to get request id"); + error!("Agent is not running, but tried to get request id"); return 0; } diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs index fccd7ca5ed6..cb10e873a33 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs @@ -14,6 +14,7 @@ use tokio::{ select, }; use tokio_util::sync::CancellationToken; +use tracing::{error, info}; use windows::Win32::{Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId}; use crate::ssh_agent::peerinfo::{self, models::PeerInfo}; @@ -31,42 +32,38 @@ impl NamedPipeServerStream { pub fn new(cancellation_token: CancellationToken, is_running: Arc) -> Self { let (tx, rx) = tokio::sync::mpsc::channel(16); tokio::spawn(async move { - println!( - "[SSH Agent Native Module] Creating named pipe server on {}", - PIPE_NAME - ); + info!("Creating named pipe server on {}", PIPE_NAME); let mut listener = match ServerOptions::new().create(PIPE_NAME) { Ok(pipe) => pipe, - Err(err) => { - println!("[SSH Agent Native Module] Encountered an error creating the first pipe. The system's openssh service must likely be disabled"); - println!("[SSH Agent Natvie Module] error: {}", err); + Err(e) => { + error!(error = %e, "Encountered an error creating the first pipe. The system's openssh service must likely be disabled"); cancellation_token.cancel(); is_running.store(false, Ordering::Relaxed); return; } }; loop { - println!("[SSH Agent Native Module] Waiting for connection"); + info!("Waiting for connection"); select! { _ = cancellation_token.cancelled() => { - println!("[SSH Agent Native Module] Cancellation token triggered, stopping named pipe server"); + info!("[SSH Agent Native Module] Cancellation token triggered, stopping named pipe server"); break; } _ = listener.connect() => { - println!("[SSH Agent Native Module] Incoming connection"); + info!("[SSH Agent Native Module] Incoming connection"); let handle = HANDLE(listener.as_raw_handle()); let mut pid = 0; unsafe { if let Err(e) = GetNamedPipeClientProcessId(handle, &mut pid) { - println!("Error getting named pipe client process id {}", e); + error!(error = %e, pid, "Faile to get named pipe client process id"); continue } }; let peer_info = peerinfo::gather::get_peer_info(pid); let peer_info = match peer_info { - Err(err) => { - println!("Failed getting process info for pid {} {}", pid, err); + Err(e) => { + error!(error = %e, pid = %pid, "Failed getting process info"); continue }, Ok(info) => info, @@ -76,8 +73,8 @@ impl NamedPipeServerStream { listener = match ServerOptions::new().create(PIPE_NAME) { Ok(pipe) => pipe, - Err(err) => { - println!("[SSH Agent Native Module] Encountered an error creating a new pipe {}", err); + Err(e) => { + error!(error = %e, "Encountered an error creating a new pipe"); cancellation_token.cancel(); is_running.store(false, Ordering::Relaxed); return; diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs index 53142d4c476..813ebd61cc1 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs @@ -12,6 +12,7 @@ use bitwarden_russh::ssh_agent; use homedir::my_home; use tokio::{net::UnixListener, sync::Mutex}; use tokio_util::sync::CancellationToken; +use tracing::{error, info}; use crate::ssh_agent::peercred_unix_listener_stream::PeercredUnixListenerStream; @@ -36,14 +37,12 @@ impl BitwardenDesktopAgent { let ssh_path = match std::env::var("BITWARDEN_SSH_AUTH_SOCK") { Ok(path) => path, Err(_) => { - println!("[SSH Agent Native Module] BITWARDEN_SSH_AUTH_SOCK not set, using default path"); + info!("BITWARDEN_SSH_AUTH_SOCK not set, using default path"); let ssh_agent_directory = match my_home() { Ok(Some(home)) => home, _ => { - println!( - "[SSH Agent Native Module] Could not determine home directory" - ); + info!("Could not determine home directory"); return; } }; @@ -65,10 +64,10 @@ impl BitwardenDesktopAgent { } }; - println!("[SSH Agent Native Module] Starting SSH Agent server on {ssh_path:?}"); + info!(socket = %ssh_path, "Starting SSH Agent server"); let sockname = std::path::Path::new(&ssh_path); if let Err(e) = std::fs::remove_file(sockname) { - println!("[SSH Agent Native Module] Could not remove existing socket file: {e}"); + error!(error = %e, socket = %ssh_path, "Could not remove existing socket file"); if e.kind() != std::io::ErrorKind::NotFound { return; } @@ -79,7 +78,7 @@ impl BitwardenDesktopAgent { // Only the current user should be able to access the socket if let Err(e) = fs::set_permissions(sockname, fs::Permissions::from_mode(0o600)) { - println!("[SSH Agent Native Module] Could not set socket permissions: {e}"); + error!(error = %e, socket = ?sockname, "Could not set socket permissions"); return; } @@ -100,10 +99,10 @@ impl BitwardenDesktopAgent { cloned_agent_state .is_running .store(false, std::sync::atomic::Ordering::Relaxed); - println!("[SSH Agent Native Module] SSH Agent server exited"); + info!("SSH Agent server exited"); } Err(e) => { - eprintln!("[SSH Agent Native Module] Error while starting agent server: {e}"); + error!(error = %e, socket = %ssh_path, "Unable to start start agent server"); } } }); From 25020ced5d18f30965e46ddb87df4c77c2d176d2 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:37:00 +0200 Subject: [PATCH 39/55] [PM-23251] Remove low-kdf banner (#16511) * Remove low-kdf banner * update tests --- .../services/vault-banners.service.spec.ts | 71 +------------------ .../services/vault-banners.service.ts | 33 +-------- .../vault-banners.component.html | 12 ---- .../vault-banners.component.spec.ts | 7 -- .../vault-banners/vault-banners.component.ts | 2 - apps/web/src/locales/en/messages.json | 12 ---- 6 files changed, 2 insertions(+), 135 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts index 89a3757e939..6b46cd89956 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts @@ -1,14 +1,8 @@ import { TestBed } from "@angular/core/testing"; import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs"; -import { - AuthRequestServiceAbstraction, - UserDecryptionOptions, - UserDecryptionOptionsServiceAbstraction, -} from "@bitwarden/auth/common"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; -import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DeviceType } from "@bitwarden/common/enums"; @@ -18,7 +12,6 @@ import { StateProvider } from "@bitwarden/common/platform/state"; import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { KdfConfigService, KdfType } from "@bitwarden/key-management"; import { PREMIUM_BANNER_REPROMPT_KEY, @@ -34,14 +27,9 @@ describe("VaultBannersService", () => { const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); const getEmailVerified = jest.fn().mockResolvedValue(true); const lastSync$ = new BehaviorSubject(null); - const userDecryptionOptions$ = new BehaviorSubject({ - hasMasterPassword: true, - }); - const kdfConfig$ = new BehaviorSubject({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600000 }); const accounts$ = new BehaviorSubject>({ [userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo, }); - const devices$ = new BehaviorSubject([]); const pendingAuthRequests$ = new BehaviorSubject>([]); beforeEach(() => { @@ -64,32 +52,14 @@ describe("VaultBannersService", () => { provide: StateProvider, useValue: fakeStateProvider, }, - { - provide: PlatformUtilsService, - useValue: { isSelfHost }, - }, { provide: AccountService, useValue: { accounts$ }, }, - { - provide: KdfConfigService, - useValue: { getKdfConfig$: () => kdfConfig$ }, - }, { provide: SyncService, useValue: { lastSync$: () => lastSync$ }, }, - { - provide: UserDecryptionOptionsServiceAbstraction, - useValue: { - userDecryptionOptionsById$: () => userDecryptionOptions$, - }, - }, - { - provide: DevicesServiceAbstraction, - useValue: { getDevices$: () => devices$ }, - }, { provide: AuthRequestServiceAbstraction, useValue: { getPendingAuthRequests$: () => pendingAuthRequests$ }, @@ -197,45 +167,6 @@ describe("VaultBannersService", () => { }); }); - describe("KDFSettings", () => { - beforeEach(async () => { - userDecryptionOptions$.next({ hasMasterPassword: true }); - kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 }); - }); - - it("shows low KDF iteration banner", async () => { - service = TestBed.inject(VaultBannersService); - - expect(await service.shouldShowLowKDFBanner(userId)).toBe(true); - }); - - it("does not show low KDF iteration banner if KDF type is not PBKDF2_SHA256", async () => { - kdfConfig$.next({ kdfType: KdfType.Argon2id, iterations: 600001 }); - - service = TestBed.inject(VaultBannersService); - - expect(await service.shouldShowLowKDFBanner(userId)).toBe(false); - }); - - it("does not show low KDF for iterations about 600,000", async () => { - kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 }); - - service = TestBed.inject(VaultBannersService); - - expect(await service.shouldShowLowKDFBanner(userId)).toBe(false); - }); - - it("dismisses low KDF iteration banner", async () => { - service = TestBed.inject(VaultBannersService); - - expect(await service.shouldShowLowKDFBanner(userId)).toBe(true); - - await service.dismissBanner(userId, VisibleVaultBanner.KDFSettings); - - expect(await service.shouldShowLowKDFBanner(userId)).toBe(false); - }); - }); - describe("OutdatedBrowser", () => { beforeEach(async () => { // Hardcode `MSIE` in userAgent string diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index c4396940998..1c53274d9d7 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -1,10 +1,7 @@ import { Injectable } from "@angular/core"; import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs"; -import { - AuthRequestServiceAbstraction, - UserDecryptionOptionsServiceAbstraction, -} from "@bitwarden/auth/common"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -18,10 +15,8 @@ import { import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; -import { PBKDF2KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management"; export const VisibleVaultBanner = { - KDFSettings: "kdf-settings", OutdatedBrowser: "outdated-browser", Premium: "premium", VerifyEmail: "verify-email", @@ -64,9 +59,7 @@ export class VaultBannersService { private stateProvider: StateProvider, private billingAccountProfileStateService: BillingAccountProfileStateService, private platformUtilsService: PlatformUtilsService, - private kdfConfigService: KdfConfigService, private syncService: SyncService, - private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, ) {} @@ -133,21 +126,6 @@ export class VaultBannersService { return needsVerification && !alreadyDismissed; } - /** Returns true when the low KDF iteration banner should be shown */ - async shouldShowLowKDFBanner(userId: UserId): Promise { - const hasLowKDF = ( - await firstValueFrom(this.userDecryptionOptionsService.userDecryptionOptionsById$(userId)) - )?.hasMasterPassword - ? await this.isLowKdfIteration(userId) - : false; - - const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes( - VisibleVaultBanner.KDFSettings, - ); - - return hasLowKDF && !alreadyDismissed; - } - /** Dismiss the given banner and perform any respective side effects */ async dismissBanner(userId: UserId, banner: SessionBanners): Promise { if (banner === VisibleVaultBanner.Premium) { @@ -221,13 +199,4 @@ export class VaultBannersService { }; }); } - - private async isLowKdfIteration(userId: UserId) { - const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId)); - return ( - kdfConfig != null && - kdfConfig.kdfType === KdfType.PBKDF2_SHA256 && - kdfConfig.iterations < PBKDF2KdfConfig.ITERATIONS.defaultValue - ); - } } diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html index d52ea9f61e6..44b2975ee19 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html @@ -25,18 +25,6 @@ - - {{ "lowKDFIterationsBanner" | i18n }} - - {{ "changeKDFSettings" | i18n }} - - - { shouldShowPremiumBanner$: jest.fn((userId: UserId) => premiumBanner$), shouldShowUpdateBrowserBanner: jest.fn(), shouldShowVerifyEmailBanner: jest.fn(), - shouldShowLowKDFBanner: jest.fn(), shouldShowPendingAuthRequestBanner: jest.fn((userId: UserId) => Promise.resolve(pendingAuthRequest$.value), ), @@ -48,7 +47,6 @@ describe("VaultBannersComponent", () => { messageSubject = new Subject<{ command: string }>(); bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false); bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false); - bannerService.shouldShowLowKDFBanner.mockResolvedValue(false); pendingAuthRequest$.next(false); premiumBanner$.next(false); @@ -137,11 +135,6 @@ describe("VaultBannersComponent", () => { method: bannerService.shouldShowVerifyEmailBanner, banner: VisibleVaultBanner.VerifyEmail, }, - { - name: "LowKDF", - method: bannerService.shouldShowLowKDFBanner, - banner: VisibleVaultBanner.KDFSettings, - }, ].forEach(({ name, method, banner }) => { describe(name, () => { beforeEach(async () => { diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts index a16374f19b3..011e7a3bce6 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts @@ -100,14 +100,12 @@ export class VaultBannersComponent implements OnInit { const showBrowserOutdated = await this.vaultBannerService.shouldShowUpdateBrowserBanner(activeUserId); const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner(activeUserId); - const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner(activeUserId); const showPendingAuthRequest = await this.vaultBannerService.shouldShowPendingAuthRequestBanner(activeUserId); this.visibleBanners = [ showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null, showVerifyEmail ? VisibleVaultBanner.VerifyEmail : null, - showLowKdf ? VisibleVaultBanner.KDFSettings : null, showPendingAuthRequest ? VisibleVaultBanner.PendingAuthRequest : null, ].filter((banner) => banner !== null); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 1b374c97478..e2bb463c939 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8408,12 +8408,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -10031,12 +10025,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, From c93586a0aab2b1654a196475ac25d2686d84d035 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:56:29 +0200 Subject: [PATCH 40/55] [deps] Tools: Update jsdom to v27 (#16634) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 242 +++++++++++++++++++++++++++++------------- package.json | 2 +- 3 files changed, 169 insertions(+), 77 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 659a68d13a5..4ed72a9c21b 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -73,7 +73,7 @@ "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jsdom": "26.1.0", + "jsdom": "27.0.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", diff --git a/package-lock.json b/package-lock.json index 1b126255e63..5c325844a94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jsdom": "26.1.0", + "jsdom": "27.0.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", @@ -208,7 +208,7 @@ "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jsdom": "26.1.0", + "jsdom": "27.0.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", @@ -2586,23 +2586,54 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" } }, "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.5.6.tgz", + "integrity": "sha512-Mj3Hu9ymlsERd7WOsUKNUZnJYL4IZ/I9wVVYgtvOsWYiEFbkQ4G7VRIh2USxTVW4BBDIsLG+gBUgqOqf2Kvqow==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" }, "node_modules/@babel/code-frame": { "version": "7.27.1", @@ -5378,9 +5409,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", - "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "funding": [ { "type": "github", @@ -5420,9 +5451,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", - "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "funding": [ { "type": "github", @@ -5435,7 +5466,7 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.0.2", + "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "engines": { @@ -5468,6 +5499,28 @@ "@csstools/css-tokenizer": "^3.0.4" } }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", + "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/@csstools/css-tokenizer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", @@ -16919,6 +16972,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -19109,6 +19171,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -19150,16 +19225,17 @@ "license": "MIT" }, "node_modules/cssstyle": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.5.0.tgz", - "integrity": "sha512-/7gw8TGrvH/0g564EnhgFZogTMVe+lifpB7LWU+PEsiq5o83TUXR3fDbzTRXOJhoJwck5IS9ez3Em5LNMMO2aw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", + "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/csstype": { @@ -19183,16 +19259,16 @@ } }, "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" + "whatwg-url": "^15.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/data-view-buffer": { @@ -26910,34 +26986,34 @@ } }, "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==", "license": "MIT", "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", + "@asamuzakjp/dom-selector": "^6.5.4", + "cssstyle": "^5.3.0", + "data-urls": "^6.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", + "parse5": "^7.3.0", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", + "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", + "webidl-conversions": "^8.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", + "whatwg-url": "^15.0.0", + "ws": "^8.18.2", "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "peerDependencies": { "canvas": "^3.0.0" @@ -26949,35 +27025,38 @@ } }, "node_modules/jsdom/node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz", + "integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.86" + "tldts-core": "^7.0.16" }, "bin": { "tldts": "bin/cli.js" } }, - "node_modules/jsdom/node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "license": "MIT" - }, "node_modules/jsdom/node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", "license": "BSD-3-Clause", "dependencies": { - "tldts": "^6.1.32" + "tldts": "^7.0.5" }, "engines": { "node": ">=16" } }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -28937,6 +29016,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -30268,7 +30353,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -31424,6 +31508,7 @@ "version": "2.2.20", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, "license": "MIT" }, "node_modules/nx": { @@ -33413,7 +33498,6 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -34711,7 +34795,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -36071,7 +36154,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -37597,9 +37679,9 @@ } }, "node_modules/tldts-core": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.9.tgz", - "integrity": "sha512-/FGY1+CryHsxF9SFiPZlMOcwQsfABkAvOJO5VEKE8TNifVEqgMF7+UVXHGhm1z4gPUfvVS/EYcwhiRU3vUa1ag==", + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz", + "integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==", "license": "MIT" }, "node_modules/tmp": { @@ -37673,15 +37755,15 @@ } }, "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/tree-dump": { @@ -39867,6 +39949,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -40807,16 +40890,25 @@ } }, "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", "license": "MIT", "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/whatwg-url/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" } }, "node_modules/which": { diff --git a/package.json b/package.json index e94d0e98522..f5894a04da9 100644 --- a/package.json +++ b/package.json @@ -180,7 +180,7 @@ "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jsdom": "26.1.0", + "jsdom": "27.0.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", From 7848b7d480b7489cf9a99543088c920613545ab5 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:40:00 +0200 Subject: [PATCH 41/55] Revert "[deps] Tools: Update jsdom to v27 (#16634)" (#16666) This reverts commit c93586a0aab2b1654a196475ac25d2686d84d035. --- apps/cli/package.json | 2 +- package-lock.json | 242 +++++++++++++----------------------------- package.json | 2 +- 3 files changed, 77 insertions(+), 169 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 4ed72a9c21b..659a68d13a5 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -73,7 +73,7 @@ "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jsdom": "27.0.0", + "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", diff --git a/package-lock.json b/package-lock.json index 5c325844a94..1b126255e63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jsdom": "27.0.0", + "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", @@ -208,7 +208,7 @@ "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jsdom": "27.0.0", + "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", @@ -2586,54 +2586,23 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", - "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "lru-cache": "^11.2.1" + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" } }, "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "6.5.6", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.5.6.tgz", - "integrity": "sha512-Mj3Hu9ymlsERd7WOsUKNUZnJYL4IZ/I9wVVYgtvOsWYiEFbkQ4G7VRIh2USxTVW4BBDIsLG+gBUgqOqf2Kvqow==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.1" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "license": "MIT" + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, "node_modules/@babel/code-frame": { "version": "7.27.1", @@ -5409,9 +5378,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", "funding": [ { "type": "github", @@ -5451,9 +5420,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", "funding": [ { "type": "github", @@ -5466,7 +5435,7 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.1.0", + "@csstools/color-helpers": "^5.0.2", "@csstools/css-calc": "^2.1.4" }, "engines": { @@ -5499,28 +5468,6 @@ "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", - "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, "node_modules/@csstools/css-tokenizer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", @@ -16972,15 +16919,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -19171,19 +19109,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -19225,17 +19150,16 @@ "license": "MIT" }, "node_modules/cssstyle": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", - "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.5.0.tgz", + "integrity": "sha512-/7gw8TGrvH/0g564EnhgFZogTMVe+lifpB7LWU+PEsiq5o83TUXR3fDbzTRXOJhoJwck5IS9ez3Em5LNMMO2aw==", "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^4.0.3", - "@csstools/css-syntax-patches-for-csstree": "^1.0.14", - "css-tree": "^3.1.0" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/csstype": { @@ -19259,16 +19183,16 @@ } }, "node_modules/data-urls": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", - "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.0.0" + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/data-view-buffer": { @@ -26986,34 +26910,34 @@ } }, "node_modules/jsdom": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz", - "integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "license": "MIT", "dependencies": { - "@asamuzakjp/dom-selector": "^6.5.4", - "cssstyle": "^5.3.0", - "data-urls": "^6.0.0", + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "parse5": "^7.3.0", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", + "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.0", + "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.0.0", - "ws": "^8.18.2", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=20" + "node": ">=18" }, "peerDependencies": { "canvas": "^3.0.0" @@ -27025,38 +26949,35 @@ } }, "node_modules/jsdom/node_modules/tldts": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz", - "integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==", + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "license": "MIT", "dependencies": { - "tldts-core": "^7.0.16" + "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, + "node_modules/jsdom/node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, "node_modules/jsdom/node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "license": "BSD-3-Clause", "dependencies": { - "tldts": "^7.0.5" + "tldts": "^6.1.32" }, "engines": { "node": ">=16" } }, - "node_modules/jsdom/node_modules/webidl-conversions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", - "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -29016,12 +28937,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "license": "CC0-1.0" - }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -30353,6 +30268,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -31508,7 +31424,6 @@ "version": "2.2.20", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", - "dev": true, "license": "MIT" }, "node_modules/nx": { @@ -33498,6 +33413,7 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -34795,6 +34711,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -36154,6 +36071,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -37679,9 +37597,9 @@ } }, "node_modules/tldts-core": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz", - "integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.9.tgz", + "integrity": "sha512-/FGY1+CryHsxF9SFiPZlMOcwQsfABkAvOJO5VEKE8TNifVEqgMF7+UVXHGhm1z4gPUfvVS/EYcwhiRU3vUa1ag==", "license": "MIT" }, "node_modules/tmp": { @@ -37755,15 +37673,15 @@ } }, "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/tree-dump": { @@ -39949,7 +39867,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -40890,25 +40807,16 @@ } }, "node_modules/whatwg-url": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", - "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.0" + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url/node_modules/webidl-conversions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", - "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/which": { diff --git a/package.json b/package.json index f5894a04da9..e94d0e98522 100644 --- a/package.json +++ b/package.json @@ -180,7 +180,7 @@ "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jsdom": "27.0.0", + "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", From 727689d827589e1fccb5eed88ef017be83da8988 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 30 Sep 2025 09:45:04 -0500 Subject: [PATCH 42/55] [PM-24534] Archive via CLI (#16502) * refactor `canInteract` into a component level usage. - The default service is going to be used in the CLI which won't make use of the UI-related aspects * all nested entities to be imported from the vault * initial add of archive command to the cli * add archive to oss serve * check for deleted cipher when attempting to archive * add searchability/list functionality for archived ciphers * restore an archived cipher * unarchive a cipher when a user is editing it and has lost their premium status * add missing feature flags * re-export only needed services from the vault * add needed await * add prompt when applicable for editing an archived cipher * move cipher archive service into `common/vault` * fix testing code --- .../src/popup/services/services.module.ts | 13 +-- .../item-more-options.component.ts | 3 +- .../vault-popup-items.service.spec.ts | 2 +- .../services/vault-popup-items.service.ts | 2 +- .../vault/popup/settings/archive.component.ts | 35 +++++- .../settings/vault-settings-v2.component.ts | 2 +- apps/cli/src/commands/edit.command.ts | 50 ++++++++ apps/cli/src/commands/list.command.ts | 34 +++++- apps/cli/src/commands/restore.command.ts | 33 +++++- apps/cli/src/commands/serve.command.ts | 2 +- apps/cli/src/oss-serve-configurator.ts | 34 +++++- apps/cli/src/program.ts | 8 ++ apps/cli/src/register-oss-programs.ts | 2 +- .../service-container/service-container.ts | 10 ++ apps/cli/src/vault.program.ts | 79 +++++++++++-- apps/cli/src/vault/archive.command.ts | 109 ++++++++++++++++++ .../vault-filter/vault-filter.component.ts | 2 +- .../components/vault-filter.component.ts | 2 +- .../vault/individual-vault/vault.component.ts | 2 +- .../bit-cli/src/bit-serve-configurator.ts | 4 +- .../src/services/jslib-services.module.ts | 11 +- .../abstractions/cipher-archive.service.ts | 2 - .../src/vault/abstractions/search.service.ts | 1 + .../default-cipher-archive.service.spec.ts | 53 --------- .../default-cipher-archive.service.ts | 25 +--- .../src/vault/services/search.service.ts | 10 +- libs/vault/src/index.ts | 2 - 27 files changed, 401 insertions(+), 131 deletions(-) create mode 100644 apps/cli/src/vault/archive.command.ts rename libs/{vault/src => common/src/vault}/abstractions/cipher-archive.service.ts (81%) rename libs/{vault/src => common/src/vault}/services/default-cipher-archive.service.spec.ts (78%) rename libs/{vault/src => common/src/vault}/services/default-cipher-archive.service.ts (83%) diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index d87c9417c85..ef4dd0be090 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -119,10 +119,12 @@ import { SystemNotificationsService } from "@bitwarden/common/platform/system-no import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; +import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { @@ -145,8 +147,6 @@ import { DefaultSshImportPromptService, PasswordRepromptService, SshImportPromptService, - CipherArchiveService, - DefaultCipherArchiveService, } from "@bitwarden/vault"; import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service"; @@ -708,14 +708,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: CipherArchiveService, useClass: DefaultCipherArchiveService, - deps: [ - CipherService, - ApiService, - DialogService, - PasswordRepromptService, - BillingAccountProfileStateService, - ConfigService, - ], + deps: [CipherService, ApiService, BillingAccountProfileStateService, ConfigService], }), ]; diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index 6979f519f2d..324ea2ffcdf 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -13,6 +13,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; @@ -28,7 +29,7 @@ import { MenuModule, ToastService, } from "@bitwarden/components"; -import { CipherArchiveService, PasswordRepromptService } from "@bitwarden/vault"; +import { PasswordRepromptService } from "@bitwarden/vault"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 6499719b64f..513e159f7aa 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -14,6 +14,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { mockAccountServiceWith, ObservableTracker } from "@bitwarden/common/spec"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; @@ -26,7 +27,6 @@ import { RestrictedItemTypesService, } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; -import { CipherArchiveService } from "@bitwarden/vault"; import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 3e4b793737e..fa56b45c080 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -26,6 +26,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; @@ -35,7 +36,6 @@ import { CipherViewLike, CipherViewLikeUtils, } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; -import { CipherArchiveService } from "@bitwarden/vault"; import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator"; import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service"; diff --git a/apps/browser/src/vault/popup/settings/archive.component.ts b/apps/browser/src/vault/popup/settings/archive.component.ts index c3e078a9274..d685beb0287 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.ts +++ b/apps/browser/src/vault/popup/settings/archive.component.ts @@ -9,6 +9,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -22,7 +23,11 @@ import { ToastService, TypographyModule, } from "@bitwarden/components"; -import { CanDeleteCipherDirective, CipherArchiveService } from "@bitwarden/vault"; +import { + CanDeleteCipherDirective, + DecryptionFailureDialogComponent, + PasswordRepromptService, +} from "@bitwarden/vault"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -56,6 +61,7 @@ export class ArchiveComponent { private toastService = inject(ToastService); private i18nService = inject(I18nService); private cipherArchiveService = inject(CipherArchiveService); + private passwordRepromptService = inject(PasswordRepromptService); private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); @@ -69,7 +75,7 @@ export class ArchiveComponent { ); async view(cipher: CipherView) { - if (!(await this.cipherArchiveService.canInteract(cipher))) { + if (!(await this.canInteract(cipher))) { return; } @@ -79,7 +85,7 @@ export class ArchiveComponent { } async edit(cipher: CipherView) { - if (!(await this.cipherArchiveService.canInteract(cipher))) { + if (!(await this.canInteract(cipher))) { return; } @@ -89,7 +95,7 @@ export class ArchiveComponent { } async delete(cipher: CipherView) { - if (!(await this.cipherArchiveService.canInteract(cipher))) { + if (!(await this.canInteract(cipher))) { return; } const confirmed = await this.dialogService.openSimpleDialog({ @@ -118,7 +124,7 @@ export class ArchiveComponent { } async unarchive(cipher: CipherView) { - if (!(await this.cipherArchiveService.canInteract(cipher))) { + if (!(await this.canInteract(cipher))) { return; } const activeUserId = await firstValueFrom(this.userId$); @@ -132,7 +138,7 @@ export class ArchiveComponent { } async clone(cipher: CipherView) { - if (!(await this.cipherArchiveService.canInteract(cipher))) { + if (!(await this.canInteract(cipher))) { return; } @@ -156,4 +162,21 @@ export class ArchiveComponent { }, }); } + + /** + * Check if the user is able to interact with the cipher + * (password re-prompt / decryption failure checks). + * @param cipher + * @private + */ + private canInteract(cipher: CipherView) { + if (cipher.decryptionFailure) { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: [cipher.id as CipherId], + }); + return false; + } + + return this.passwordRepromptService.passwordRepromptCheck(cipher); + } } diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts index 4e8a49b2591..92cbf951ead 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts @@ -9,9 +9,9 @@ import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components"; -import { CipherArchiveService } from "@bitwarden/vault"; import { BrowserApi } from "../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 92674aa3dcd..f4216196ead 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import * as inquirer from "inquirer"; import { firstValueFrom, map, switchMap } from "rxjs"; import { UpdateCollectionRequest } from "@bitwarden/admin-console/common"; @@ -9,6 +10,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; @@ -40,6 +42,7 @@ export class EditCommand { private accountService: AccountService, private cliRestrictedItemTypesService: CliRestrictedItemTypesService, private policyService: PolicyService, + private billingAccountProfileStateService: BillingAccountProfileStateService, ) {} async run( @@ -92,6 +95,10 @@ export class EditCommand { private async editCipher(id: string, req: CipherExport) { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipher = await this.cipherService.get(id, activeUserId); + const hasPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), + ); + if (cipher == null) { return Response.notFound(); } @@ -102,6 +109,17 @@ export class EditCommand { } cipherView = CipherExport.toView(req, cipherView); + // When a user is editing an archived cipher and does not have premium, automatically unarchive it + if (cipherView.isArchived && !hasPremium) { + const acceptedPrompt = await this.promptForArchiveEdit(); + + if (!acceptedPrompt) { + return Response.error("Edit cancelled."); + } + + cipherView.archivedDate = null; + } + const isCipherRestricted = await this.cliRestrictedItemTypesService.isCipherRestricted(cipherView); if (isCipherRestricted) { @@ -240,6 +258,38 @@ export class EditCommand { return Response.error(e); } } + + /** Prompt the user to accept movement of their cipher back to the their vault. */ + private async promptForArchiveEdit(): Promise { + // When running in serve or no interaction mode, automatically accept the prompt + if (process.env.BW_SERVE === "true" || process.env.BW_NOINTERACTION === "true") { + CliUtils.writeLn( + "Archive is only available with a Premium subscription, which has ended. Your edit was saved and the item was moved back to your vault.", + ); + return true; + } + + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "list", + name: "confirm", + message: + "When you edit and save details for an archived item without a Premium subscription, it'll be moved from your archive back to your vault.", + choices: [ + { + name: "Move now", + value: "confirmed", + }, + { + name: "Cancel", + value: "cancel", + }, + ], + }); + + return answer.confirm === "confirmed"; + } } class Options { diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index d8b4cfcfd10..49527f6bf78 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -16,6 +16,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; @@ -45,6 +46,7 @@ export class ListCommand { private accountService: AccountService, private keyService: KeyService, private cliRestrictedItemTypesService: CliRestrictedItemTypesService, + private cipherArchiveService: CipherArchiveService, ) {} async run(object: string, cmdOptions: Record): Promise { @@ -71,8 +73,13 @@ export class ListCommand { let ciphers: CipherView[]; const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const userCanArchive = await firstValueFrom( + this.cipherArchiveService.userCanArchive$(activeUserId), + ); options.trash = options.trash || false; + options.archived = userCanArchive && options.archived; + if (options.url != null && options.url.trim() !== "") { ciphers = await this.cipherService.getAllDecryptedForUrl(options.url, activeUserId); } else { @@ -85,9 +92,12 @@ export class ListCommand { options.organizationId != null ) { ciphers = ciphers.filter((c) => { - if (options.trash !== c.isDeleted) { + const matchesStateOptions = this.matchesStateOptions(c, options); + + if (!matchesStateOptions) { return false; } + if (options.folderId != null) { if (options.folderId === "notnull" && c.folderId != null) { return true; @@ -131,11 +141,16 @@ export class ListCommand { return false; }); } else if (options.search == null || options.search.trim() === "") { - ciphers = ciphers.filter((c) => options.trash === c.isDeleted); + ciphers = ciphers.filter((c) => this.matchesStateOptions(c, options)); } if (options.search != null && options.search.trim() !== "") { - ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash); + ciphers = this.searchService.searchCiphersBasic( + ciphers, + options.search, + options.trash, + options.archived, + ); } ciphers = await this.cliRestrictedItemTypesService.filterRestrictedCiphers(ciphers); @@ -287,6 +302,17 @@ export class ListCommand { const res = new ListResponse(organizations.map((o) => new OrganizationResponse(o))); return Response.success(res); } + + /** + * Checks if the cipher passes either the trash or the archive options. + * @returns true if the cipher passes *any* of the filters + */ + private matchesStateOptions(c: CipherView, options: Options): boolean { + const passesTrashFilter = options.trash && c.isDeleted; + const passesArchivedFilter = options.archived && c.isArchived; + + return passesTrashFilter || passesArchivedFilter; + } } class Options { @@ -296,6 +322,7 @@ class Options { search: string; url: string; trash: boolean; + archived: boolean; constructor(passedOptions: Record) { this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId; @@ -304,5 +331,6 @@ class Options { this.search = passedOptions?.search; this.url = passedOptions?.url; this.trash = CliUtils.convertBooleanOption(passedOptions?.trash); + this.archived = CliUtils.convertBooleanOption(passedOptions?.archived); } } diff --git a/apps/cli/src/commands/restore.command.ts b/apps/cli/src/commands/restore.command.ts index 0b30193ffd4..d8cefdfce5d 100644 --- a/apps/cli/src/commands/restore.command.ts +++ b/apps/cli/src/commands/restore.command.ts @@ -2,8 +2,14 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { UserId } from "@bitwarden/user-core"; import { Response } from "../models/response"; @@ -12,6 +18,8 @@ export class RestoreCommand { private cipherService: CipherService, private accountService: AccountService, private cipherAuthorizationService: CipherAuthorizationService, + private cipherArchiveService: CipherArchiveService, + private configService: ConfigService, ) {} async run(object: string, id: string): Promise { @@ -30,10 +38,23 @@ export class RestoreCommand { private async restoreCipher(id: string) { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipher = await this.cipherService.get(id, activeUserId); + const isArchivedVaultEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive), + ); if (cipher == null) { return Response.notFound(); } + + if (cipher.archivedDate && isArchivedVaultEnabled) { + return this.restoreArchivedCipher(cipher, activeUserId); + } else { + return this.restoreDeletedCipher(cipher, activeUserId); + } + } + + /** Restores a cipher from the trash. */ + private async restoreDeletedCipher(cipher: Cipher, userId: UserId) { if (cipher.deletedDate == null) { return Response.badRequest("Cipher is not in trash."); } @@ -47,7 +68,17 @@ export class RestoreCommand { } try { - await this.cipherService.restoreWithServer(id, activeUserId); + await this.cipherService.restoreWithServer(cipher.id, userId); + return Response.success(); + } catch (e) { + return Response.error(e); + } + } + + /** Restore a cipher from the archive vault */ + private async restoreArchivedCipher(cipher: Cipher, userId: UserId) { + try { + await this.cipherArchiveService.unarchiveWithServer(cipher.id as CipherId, userId); return Response.success(); } catch (e) { return Response.error(e); diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index c0ec37d3c9c..5bf19333f35 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -51,7 +51,7 @@ export class ServeCommand { .use(koaBodyParser()) .use(koaJson({ pretty: false, param: "pretty" })); - this.serveConfigurator.configureRouter(router); + await this.serveConfigurator.configureRouter(router); server.use(router.routes()).use(router.allowedMethods()); diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index 6ae2776eae7..3c80d12af2f 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -5,6 +5,8 @@ import * as koaRouter from "@koa/router"; import * as koa from "koa"; import { firstValueFrom, map } from "rxjs"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + import { ConfirmCommand } from "./admin-console/commands/confirm.command"; import { ShareCommand } from "./admin-console/commands/share.command"; import { LockCommand } from "./auth/commands/lock.command"; @@ -26,6 +28,7 @@ import { SendListCommand, SendRemovePasswordCommand, } from "./tools/send"; +import { ArchiveCommand } from "./vault/archive.command"; import { CreateCommand } from "./vault/create.command"; import { DeleteCommand } from "./vault/delete.command"; import { SyncCommand } from "./vault/sync.command"; @@ -40,6 +43,7 @@ export class OssServeConfigurator { private statusCommand: StatusCommand; private syncCommand: SyncCommand; private deleteCommand: DeleteCommand; + private archiveCommand: ArchiveCommand; private confirmCommand: ConfirmCommand; private restoreCommand: RestoreCommand; private lockCommand: LockCommand; @@ -81,6 +85,7 @@ export class OssServeConfigurator { this.serviceContainer.accountService, this.serviceContainer.keyService, this.serviceContainer.cliRestrictedItemTypesService, + this.serviceContainer.cipherArchiveService, ); this.createCommand = new CreateCommand( this.serviceContainer.cipherService, @@ -104,6 +109,7 @@ export class OssServeConfigurator { this.serviceContainer.accountService, this.serviceContainer.cliRestrictedItemTypesService, this.serviceContainer.policyService, + this.serviceContainer.billingAccountProfileStateService, ); this.generateCommand = new GenerateCommand( this.serviceContainer.passwordGenerationService, @@ -127,6 +133,13 @@ export class OssServeConfigurator { this.serviceContainer.accountService, this.serviceContainer.cliRestrictedItemTypesService, ); + this.archiveCommand = new ArchiveCommand( + this.serviceContainer.cipherService, + this.serviceContainer.accountService, + this.serviceContainer.configService, + this.serviceContainer.cipherArchiveService, + this.serviceContainer.billingAccountProfileStateService, + ); this.confirmCommand = new ConfirmCommand( this.serviceContainer.apiService, this.serviceContainer.keyService, @@ -140,6 +153,8 @@ export class OssServeConfigurator { this.serviceContainer.cipherService, this.serviceContainer.accountService, this.serviceContainer.cipherAuthorizationService, + this.serviceContainer.cipherArchiveService, + this.serviceContainer.configService, ); this.shareCommand = new ShareCommand( this.serviceContainer.cipherService, @@ -199,7 +214,7 @@ export class OssServeConfigurator { ); } - configureRouter(router: koaRouter) { + async configureRouter(router: koaRouter) { router.get("/generate", async (ctx, next) => { const response = await this.generateCommand.run(ctx.request.query); this.processResponse(ctx.response, response); @@ -401,6 +416,23 @@ export class OssServeConfigurator { this.processResponse(ctx.response, response); await next(); }); + + const isArchivedEnabled = await this.serviceContainer.configService.getFeatureFlag( + FeatureFlag.PM19148_InnovationArchive, + ); + + if (isArchivedEnabled) { + router.post("/archive/:object/:id", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + let response: Response = null; + response = await this.archiveCommand.run(ctx.params.object, ctx.params.id); + this.processResponse(ctx.response, response); + await next(); + }); + } } protected processResponse(res: koa.Response, commandResponse: Response) { diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 4d541739aab..8f202bc0845 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -5,6 +5,7 @@ import { program, Command, OptionValues } from "commander"; import { firstValueFrom, of, switchMap } from "rxjs"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockCommand } from "./auth/commands/lock.command"; import { LoginCommand } from "./auth/commands/login.command"; @@ -26,6 +27,10 @@ const writeLn = CliUtils.writeLn; export class Program extends BaseProgram { async register() { + const isArchivedEnabled = await this.serviceContainer.configService.getFeatureFlag( + FeatureFlag.PM19148_InnovationArchive, + ); + program .option("--pretty", "Format output. JSON is tabbed with two spaces.") .option("--raw", "Return raw output instead of a descriptive message.") @@ -94,6 +99,9 @@ export class Program extends BaseProgram { " bw edit folder c7c7b60b-9c61-40f2-8ccd-36c49595ed72 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==", ); writeLn(" bw delete item 99ee88d2-6046-4ea7-92c2-acac464b1412"); + if (isArchivedEnabled) { + writeLn(" bw archive item 99ee88d2-6046-4ea7-92c2-acac464b1412"); + } writeLn(" bw generate -lusn --length 18"); writeLn(" bw config server https://bitwarden.example.com"); writeLn(" bw send -f ./file.ext"); diff --git a/apps/cli/src/register-oss-programs.ts b/apps/cli/src/register-oss-programs.ts index 1fc1f0119d2..71d7aaa0d52 100644 --- a/apps/cli/src/register-oss-programs.ts +++ b/apps/cli/src/register-oss-programs.ts @@ -15,7 +15,7 @@ export async function registerOssPrograms(serviceContainer: ServiceContainer) { await program.register(); const vaultProgram = new VaultProgram(serviceContainer); - vaultProgram.register(); + await vaultProgram.register(); const sendProgram = new SendProgram(serviceContainer); sendProgram.register(); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 7b148b2a3d5..8fb48fbc1ee 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -125,6 +125,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { @@ -132,6 +133,7 @@ import { DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service"; import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; @@ -303,6 +305,7 @@ export class ServiceContainer { cipherEncryptionService: CipherEncryptionService; restrictedItemTypesService: RestrictedItemTypesService; cliRestrictedItemTypesService: CliRestrictedItemTypesService; + cipherArchiveService: CipherArchiveService; constructor() { let p = null; @@ -730,6 +733,13 @@ export class ServiceContainer { this.messagingService, ); + this.cipherArchiveService = new DefaultCipherArchiveService( + this.cipherService, + this.apiService, + this.billingAccountProfileStateService, + this.configService, + ); + this.folderService = new FolderService( this.keyService, this.encryptService, diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 5b35f6b0499..21f87feab00 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { program, Command } from "commander"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + import { ConfirmCommand } from "./admin-console/commands/confirm.command"; import { ShareCommand } from "./admin-console/commands/share.command"; import { BaseProgram } from "./base-program"; @@ -13,25 +15,34 @@ import { Response } from "./models/response"; import { ExportCommand } from "./tools/export.command"; import { ImportCommand } from "./tools/import.command"; import { CliUtils } from "./utils"; +import { ArchiveCommand } from "./vault/archive.command"; import { CreateCommand } from "./vault/create.command"; import { DeleteCommand } from "./vault/delete.command"; const writeLn = CliUtils.writeLn; export class VaultProgram extends BaseProgram { - register() { + async register() { + const isArchivedEnabled = await this.serviceContainer.configService.getFeatureFlag( + FeatureFlag.PM19148_InnovationArchive, + ); + program - .addCommand(this.listCommand()) + .addCommand(this.listCommand(isArchivedEnabled)) .addCommand(this.getCommand()) .addCommand(this.createCommand()) .addCommand(this.editCommand()) .addCommand(this.deleteCommand()) - .addCommand(this.restoreCommand()) + .addCommand(this.restoreCommand(isArchivedEnabled)) .addCommand(this.shareCommand("move", false)) .addCommand(this.confirmCommand()) .addCommand(this.importCommand()) .addCommand(this.exportCommand()) .addCommand(this.shareCommand("share", true)); + + if (isArchivedEnabled) { + program.addCommand(this.archiveCommand()); + } } private validateObject(requestedObject: string, validObjects: string[]): boolean { @@ -42,7 +53,7 @@ export class VaultProgram extends BaseProgram { Response.badRequest( 'Unknown object "' + requestedObject + - '". Allowed objects are ' + + '". Allowed objects are: ' + validObjects.join(", ") + ".", ), @@ -51,7 +62,7 @@ export class VaultProgram extends BaseProgram { return success; } - private listCommand(): Command { + private listCommand(isArchivedEnabled: boolean): Command { const listObjects = [ "items", "folders", @@ -61,7 +72,7 @@ export class VaultProgram extends BaseProgram { "organizations", ]; - return new Command("list") + const command = new Command("list") .argument("", "Valid objects are: " + listObjects.join(", ")) .description("List an array of objects from the vault.") .option("--search ", "Perform a search on the listed objects.") @@ -94,6 +105,9 @@ export class VaultProgram extends BaseProgram { " bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2 --organizationid notnull", ); writeLn(" bw list items --trash"); + if (isArchivedEnabled) { + writeLn(" bw list items --archived"); + } writeLn(" bw list folders --search email"); writeLn(" bw list org-members --organizationid 60556c31-e649-4b5d-8daf-fc1c391a1bf2"); writeLn("", true); @@ -116,11 +130,18 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.accountService, this.serviceContainer.keyService, this.serviceContainer.cliRestrictedItemTypesService, + this.serviceContainer.cipherArchiveService, ); const response = await command.run(object, cmd); this.processResponse(response); }); + + if (isArchivedEnabled) { + command.option("--archived", "Filter items that are archived."); + } + + return command; } private getCommand(): Command { @@ -286,6 +307,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.accountService, this.serviceContainer.cliRestrictedItemTypesService, this.serviceContainer.policyService, + this.serviceContainer.billingAccountProfileStateService, ); const response = await command.run(object, id, encodedJson, cmd); this.processResponse(response); @@ -336,12 +358,41 @@ export class VaultProgram extends BaseProgram { }); } - private restoreCommand(): Command { + private archiveCommand(): Command { + const archiveObjects = ["item"]; + return new Command("archive") + .argument("", "Valid objects are: " + archiveObjects.join(", ")) + .argument("", "Object's globally unique `id`.") + .description("Archive an object from the vault.") + .on("--help", () => { + writeLn("\n Examples:"); + writeLn(""); + writeLn(" bw archive item 7063feab-4b10-472e-b64c-785e2b870b92"); + writeLn("", true); + }) + .action(async (object, id) => { + if (!this.validateObject(object, archiveObjects)) { + return; + } + + await this.exitIfLocked(); + const command = new ArchiveCommand( + this.serviceContainer.cipherService, + this.serviceContainer.accountService, + this.serviceContainer.configService, + this.serviceContainer.cipherArchiveService, + this.serviceContainer.billingAccountProfileStateService, + ); + const response = await command.run(object, id); + this.processResponse(response); + }); + } + + private restoreCommand(isArchivedEnabled: boolean): Command { const restoreObjects = ["item"]; - return new Command("restore") + const command = new Command("restore") .argument("", "Valid objects are: " + restoreObjects.join(", ")) .argument("", "Object's globally unique `id`.") - .description("Restores an object from the trash.") .on("--help", () => { writeLn("\n Examples:"); writeLn(""); @@ -358,10 +409,20 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.cipherService, this.serviceContainer.accountService, this.serviceContainer.cipherAuthorizationService, + this.serviceContainer.cipherArchiveService, + this.serviceContainer.configService, ); const response = await command.run(object, id); this.processResponse(response); }); + + if (isArchivedEnabled) { + command.description("Restores an object from the trash or archive."); + } else { + command.description("Restores an object from the trash."); + } + + return command; } private shareCommand(commandName: string, deprecated: boolean): Command { diff --git a/apps/cli/src/vault/archive.command.ts b/apps/cli/src/vault/archive.command.ts new file mode 100644 index 00000000000..5ced2282c6d --- /dev/null +++ b/apps/cli/src/vault/archive.command.ts @@ -0,0 +1,109 @@ +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; +import { UserId } from "@bitwarden/user-core"; + +import { Response } from "../models/response"; + +export class ArchiveCommand { + constructor( + private cipherService: CipherService, + private accountService: AccountService, + private configService: ConfigService, + private cipherArchiveService: CipherArchiveService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + ) {} + + async run(object: string, id: string): Promise { + const featureFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM19148_InnovationArchive, + ); + + if (!featureFlagEnabled) { + return Response.notFound(); + } + + if (id != null) { + id = id.toLowerCase(); + } + + const normalizedObject = object.toLowerCase(); + + if (normalizedObject === "item") { + return this.archiveCipher(id); + } + + return Response.badRequest("Unknown object."); + } + + private async archiveCipher(cipherId: string) { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const cipher = await this.cipherService.get(cipherId, activeUserId); + + if (cipher == null) { + return Response.notFound(); + } + + const cipherView = await this.cipherService.decrypt(cipher, activeUserId); + + const { canArchive, errorMessage } = await this.userCanArchiveCipher(cipherView, activeUserId); + + if (!canArchive) { + return Response.error(errorMessage); + } + + try { + await this.cipherArchiveService.archiveWithServer(cipherView.id as CipherId, activeUserId); + return Response.success(); + } catch (e) { + return Response.error(e); + } + } + + /** + * Determines if the user can archive the given cipher. + * When the user cannot archive the cipher, an appropriate error message is provided. + */ + private async userCanArchiveCipher( + cipher: CipherView, + userId: UserId, + ): Promise< + { canArchive: true; errorMessage?: never } | { canArchive: false; errorMessage: string } + > { + const hasPremiumFromAnySource = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId), + ); + + switch (true) { + case !hasPremiumFromAnySource: { + return { + canArchive: false, + errorMessage: "Premium status is required to use this feature.", + }; + } + case CipherViewLikeUtils.isArchived(cipher): { + return { canArchive: false, errorMessage: "Item is already archived." }; + } + case CipherViewLikeUtils.isDeleted(cipher): { + return { + canArchive: false, + errorMessage: "Item is in the trash, the item must be restored before archiving.", + }; + } + case cipher.organizationId != null: { + return { canArchive: false, errorMessage: "Cannot archive items in an organization." }; + } + default: + return { canArchive: true }; + } + } +} diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index 1ab76c74655..3341a428970 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -9,11 +9,11 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { CipherArchiveService } from "@bitwarden/vault"; import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component"; import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 6feaa52d190..1fc3047f2a3 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -19,12 +19,12 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { CipherArchiveService } from "@bitwarden/vault"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { VaultFilterService } from "../services/abstractions/vault-filter.service"; diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 01082bfcd60..a1c44a9f623 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -54,6 +54,7 @@ import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.se import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; @@ -77,7 +78,6 @@ import { AttachmentDialogCloseResult, AttachmentDialogResult, AttachmentsV2Component, - CipherArchiveService, CipherFormConfig, CollectionAssignmentResult, DecryptionFailureDialogComponent, diff --git a/bitwarden_license/bit-cli/src/bit-serve-configurator.ts b/bitwarden_license/bit-cli/src/bit-serve-configurator.ts index c669eb70920..71df651d9d0 100644 --- a/bitwarden_license/bit-cli/src/bit-serve-configurator.ts +++ b/bitwarden_license/bit-cli/src/bit-serve-configurator.ts @@ -16,9 +16,9 @@ export class BitServeConfigurator extends OssServeConfigurator { super(serviceContainer); } - override configureRouter(router: koaRouter): void { + override async configureRouter(router: koaRouter): Promise { // Register OSS endpoints - super.configureRouter(router); + await super.configureRouter(router); // Register bit endpoints this.serveDeviceApprovals(router); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index ff704394bc3..03d756ee11c 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -264,6 +264,7 @@ import { InternalSendService, SendService as SendServiceAbstraction, } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; @@ -284,6 +285,7 @@ import { DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service"; import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; @@ -296,7 +298,6 @@ import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks"; import { AnonLayoutWrapperDataService, DefaultAnonLayoutWrapperDataService, - DialogService, ToastService, } from "@bitwarden/components"; import { @@ -345,11 +346,7 @@ import { import { SafeInjectionToken } from "@bitwarden/ui-common"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { - CipherArchiveService, - DefaultCipherArchiveService, - PasswordRepromptService, -} from "@bitwarden/vault"; +import { PasswordRepromptService } from "@bitwarden/vault"; import { IndividualVaultExportService, IndividualVaultExportServiceAbstraction, @@ -1652,8 +1649,6 @@ const safeProviders: SafeProvider[] = [ deps: [ CipherServiceAbstraction, ApiServiceAbstraction, - DialogService, - PasswordRepromptService, BillingAccountProfileStateService, ConfigService, ], diff --git a/libs/vault/src/abstractions/cipher-archive.service.ts b/libs/common/src/vault/abstractions/cipher-archive.service.ts similarity index 81% rename from libs/vault/src/abstractions/cipher-archive.service.ts rename to libs/common/src/vault/abstractions/cipher-archive.service.ts index 6240e4001c8..cb6c38ddf67 100644 --- a/libs/vault/src/abstractions/cipher-archive.service.ts +++ b/libs/common/src/vault/abstractions/cipher-archive.service.ts @@ -1,7 +1,6 @@ import { Observable } from "rxjs"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; export abstract class CipherArchiveService { @@ -10,5 +9,4 @@ export abstract class CipherArchiveService { abstract showArchiveVault$(userId: UserId): Observable; abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise; abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise; - abstract canInteract(cipher: CipherView): Promise; } diff --git a/libs/common/src/vault/abstractions/search.service.ts b/libs/common/src/vault/abstractions/search.service.ts index 6b01302613c..233dee9ec75 100644 --- a/libs/common/src/vault/abstractions/search.service.ts +++ b/libs/common/src/vault/abstractions/search.service.ts @@ -30,6 +30,7 @@ export abstract class SearchService { ciphers: C[], query: string, deleted?: boolean, + archived?: boolean, ): C[]; abstract searchSends(sends: SendView[], query: string): SendView[]; } diff --git a/libs/vault/src/services/default-cipher-archive.service.spec.ts b/libs/common/src/vault/services/default-cipher-archive.service.spec.ts similarity index 78% rename from libs/vault/src/services/default-cipher-archive.service.spec.ts rename to libs/common/src/vault/services/default-cipher-archive.service.spec.ts index ec2943ce7e4..972b04d2c4e 100644 --- a/libs/vault/src/services/default-cipher-archive.service.spec.ts +++ b/libs/common/src/vault/services/default-cipher-archive.service.spec.ts @@ -11,21 +11,14 @@ import { CipherBulkArchiveRequest, CipherBulkUnarchiveRequest, } from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { DialogService } from "@bitwarden/components"; import { CipherListView } from "@bitwarden/sdk-internal"; -import { DecryptionFailureDialogComponent } from "../components/decryption-failure-dialog/decryption-failure-dialog.component"; - import { DefaultCipherArchiveService } from "./default-cipher-archive.service"; -import { PasswordRepromptService } from "./password-reprompt.service"; describe("DefaultCipherArchiveService", () => { let service: DefaultCipherArchiveService; let mockCipherService: jest.Mocked; let mockApiService: jest.Mocked; - let mockDialogService: jest.Mocked; - let mockPasswordRepromptService: jest.Mocked; let mockBillingAccountProfileStateService: jest.Mocked; let mockConfigService: jest.Mocked; @@ -35,16 +28,12 @@ describe("DefaultCipherArchiveService", () => { beforeEach(() => { mockCipherService = mock(); mockApiService = mock(); - mockDialogService = mock(); - mockPasswordRepromptService = mock(); mockBillingAccountProfileStateService = mock(); mockConfigService = mock(); service = new DefaultCipherArchiveService( mockCipherService, mockApiService, - mockDialogService, - mockPasswordRepromptService, mockBillingAccountProfileStateService, mockConfigService, ); @@ -244,46 +233,4 @@ describe("DefaultCipherArchiveService", () => { ); }); }); - - describe("canInteract", () => { - let mockCipherView: CipherView; - - beforeEach(() => { - mockCipherView = { - id: cipherId, - decryptionFailure: false, - } as unknown as CipherView; - }); - - it("should return false and open dialog when cipher has decryption failure", async () => { - mockCipherView.decryptionFailure = true; - const openSpy = jest.spyOn(DecryptionFailureDialogComponent, "open").mockImplementation(); - - const result = await service.canInteract(mockCipherView); - - expect(result).toBe(false); - expect(openSpy).toHaveBeenCalledWith(mockDialogService, { - cipherIds: [cipherId], - }); - }); - - it("should return password reprompt result when no decryption failure", async () => { - mockPasswordRepromptService.passwordRepromptCheck.mockResolvedValue(true); - - const result = await service.canInteract(mockCipherView); - - expect(result).toBe(true); - expect(mockPasswordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith( - mockCipherView, - ); - }); - - it("should return false when password reprompt fails", async () => { - mockPasswordRepromptService.passwordRepromptCheck.mockResolvedValue(false); - - const result = await service.canInteract(mockCipherView); - - expect(result).toBe(false); - }); - }); }); diff --git a/libs/vault/src/services/default-cipher-archive.service.ts b/libs/common/src/vault/services/default-cipher-archive.service.ts similarity index 83% rename from libs/vault/src/services/default-cipher-archive.service.ts rename to libs/common/src/vault/services/default-cipher-archive.service.ts index d9a0ec54d73..5c627d687b2 100644 --- a/libs/vault/src/services/default-cipher-archive.service.ts +++ b/libs/common/src/vault/services/default-cipher-archive.service.ts @@ -12,27 +12,21 @@ import { CipherBulkUnarchiveRequest, } from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request"; import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherViewLike, CipherViewLikeUtils, } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; -import { DialogService } from "@bitwarden/components"; import { CipherArchiveService } from "../abstractions/cipher-archive.service"; -import { DecryptionFailureDialogComponent } from "../components/decryption-failure-dialog/decryption-failure-dialog.component"; - -import { PasswordRepromptService } from "./password-reprompt.service"; export class DefaultCipherArchiveService implements CipherArchiveService { constructor( private cipherService: CipherService, private apiService: ApiService, - private dialogService: DialogService, - private passwordRepromptService: PasswordRepromptService, private billingAccountProfileStateService: BillingAccountProfileStateService, private configService: ConfigService, ) {} + /** * Observable that contains the list of ciphers that have been archived. */ @@ -125,21 +119,4 @@ export class DefaultCipherArchiveService implements CipherArchiveService { await this.cipherService.replace(currentCiphers, userId); } - - /** - * Check if the user is able to interact with the cipher - * (password re-prompt / decryption failure checks). - * @param cipher - * @private - */ - async canInteract(cipher: CipherView) { - if (cipher.decryptionFailure) { - DecryptionFailureDialogComponent.open(this.dialogService, { - cipherIds: [cipher.id as CipherId], - }); - return false; - } - - return await this.passwordRepromptService.passwordRepromptCheck(cipher); - } } diff --git a/libs/common/src/vault/services/search.service.ts b/libs/common/src/vault/services/search.service.ts index cbd89cf1ab1..80fddda42d5 100644 --- a/libs/common/src/vault/services/search.service.ts +++ b/libs/common/src/vault/services/search.service.ts @@ -296,12 +296,20 @@ export class SearchService implements SearchServiceAbstraction { return results; } - searchCiphersBasic(ciphers: C[], query: string, deleted = false) { + searchCiphersBasic( + ciphers: C[], + query: string, + deleted = false, + archived = false, + ) { query = SearchService.normalizeSearchQuery(query.trim().toLowerCase()); return ciphers.filter((c) => { if (deleted !== CipherViewLikeUtils.isDeleted(c)) { return false; } + if (archived !== CipherViewLikeUtils.isArchived(c)) { + return false; + } if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) { return true; } diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 5acac9ec009..efaefc77ade 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -27,5 +27,3 @@ export { SshImportPromptService } from "./services/ssh-import-prompt.service"; export * from "./abstractions/change-login-password.service"; export * from "./services/default-change-login-password.service"; -export * from "./abstractions/cipher-archive.service"; -export * from "./services/default-cipher-archive.service"; From d4f68e8bade77061612d17394d5495f84ae1627b Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:29:58 +0200 Subject: [PATCH 43/55] [PM-25473] Non-encryption passkeys prevent key rotation (#16514) * consistent webauthn filtering as in server by prfStatus, better docs * test coverage --- ...bauthn-login-credential-prf-status.enum.ts | 9 +++ .../webauthn-login-credential.response.ts | 2 +- .../webauthn-login-admin.service.spec.ts | 79 ++++++++++++++++++- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts b/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts index 3073917e57b..02f870f094d 100644 --- a/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts +++ b/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts @@ -1,7 +1,16 @@ // FIXME: update to use a const object instead of a typescript enum // eslint-disable-next-line @bitwarden/platform/no-enums export enum WebauthnLoginCredentialPrfStatus { + /** + * Encrypted user key present, PRF function is supported. + */ Enabled = 0, + /** + * PRF function is supported. + */ Supported = 1, + /** + * PRF function is not supported. + */ Unsupported = 2, } diff --git a/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential.response.ts b/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential.response.ts index 85e7a7368e0..aba5940d752 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential.response.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential.response.ts @@ -40,6 +40,6 @@ export class WebauthnLoginCredentialResponse extends BaseResponse { } hasPrfKeyset(): boolean { - return this.encryptedUserKey != null && this.encryptedPublicKey != null; + return this.prfStatus === WebauthnLoginCredentialPrfStatus.Enabled; } } diff --git a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts index c2a9946ea38..74323773e66 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts @@ -10,15 +10,19 @@ import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/ab import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { makeSymmetricCryptoKey } from "@bitwarden/common/spec"; +import { makeEncString, makeSymmetricCryptoKey } from "@bitwarden/common/spec"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; +import { UserId } from "@bitwarden/user-core"; +import { WebauthnLoginCredentialPrfStatus } from "../../enums/webauthn-login-credential-prf-status.enum"; import { CredentialCreateOptionsView } from "../../views/credential-create-options.view"; import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view"; import { RotateableKeySetService } from "../rotateable-key-set.service"; import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request"; +import { WebauthnLoginCredentialResponse } from "./response/webauthn-login-credential.response"; import { WebAuthnLoginAdminApiService } from "./webauthn-login-admin-api.service"; import { WebauthnLoginAdminService } from "./webauthn-login-admin.service"; @@ -248,6 +252,79 @@ describe("WebauthnAdminService", () => { expect(rotateKeySetMock).not.toHaveBeenCalled(); }); }); + + describe("getRotatedData", () => { + const mockRotatedPublicKey = makeEncString("rotated_encryptedPublicKey"); + const mockRotatedUserKey = makeEncString("rotated_encryptedUserKey"); + const oldUserKey = makeSymmetricCryptoKey(64) as UserKey; + const newUserKey = makeSymmetricCryptoKey(64) as UserKey; + const userId = Utils.newGuid() as UserId; + + it("should only include credentials with PRF keysets", async () => { + const responseUnsupported = new WebauthnLoginCredentialResponse({ + id: "test-credential-id-1", + name: "Test Credential 1", + prfStatus: WebauthnLoginCredentialPrfStatus.Unsupported, + encryptedPublicKey: null, + encryptedUserKey: null, + }); + const responseSupported = new WebauthnLoginCredentialResponse({ + id: "test-credential-id-2", + name: "Test Credential 2", + prfStatus: WebauthnLoginCredentialPrfStatus.Supported, + encryptedPublicKey: null, + encryptedUserKey: null, + }); + const responseEnabled = new WebauthnLoginCredentialResponse({ + id: "test-credential-id-3", + name: "Test Credential 3", + prfStatus: WebauthnLoginCredentialPrfStatus.Enabled, + encryptedPublicKey: makeEncString("encryptedPublicKey").toJSON(), + encryptedUserKey: makeEncString("encryptedUserKey").toJSON(), + }); + + apiService.getCredentials.mockResolvedValue( + new ListResponse( + { + data: [responseUnsupported, responseSupported, responseEnabled], + }, + WebauthnLoginCredentialResponse, + ), + ); + + rotateableKeySetService.rotateKeySet.mockResolvedValue( + new RotateableKeySet(mockRotatedUserKey, mockRotatedPublicKey), + ); + + const result = await service.getRotatedData(oldUserKey, newUserKey, userId); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual( + expect.objectContaining({ + id: "test-credential-id-3", + encryptedPublicKey: mockRotatedPublicKey, + encryptedUserKey: mockRotatedUserKey, + }), + ); + expect(rotateableKeySetService.rotateKeySet).toHaveBeenCalledTimes(1); + expect(rotateableKeySetService.rotateKeySet).toHaveBeenCalledWith( + responseEnabled.getRotateableKeyset(), + oldUserKey, + newUserKey, + ); + }); + + it("should error when getCredentials fails", async () => { + const expectedError = "API connection failed"; + apiService.getCredentials.mockRejectedValue(new Error(expectedError)); + + await expect(service.getRotatedData(oldUserKey, newUserKey, userId)).rejects.toThrow( + expectedError, + ); + + expect(rotateableKeySetService.rotateKeySet).not.toHaveBeenCalled(); + }); + }); }); function createCredentialCreateOptions(): CredentialCreateOptionsView { From f793c2da09bf94cb2492c714ce5b75edc030fb9d Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 30 Sep 2025 11:33:39 -0400 Subject: [PATCH 44/55] remove feature flag (#16640) --- .../deprecated_vault.component.html | 121 -- .../collections/deprecated_vault.component.ts | 1389 ----------------- .../collections/vault-routing.module.ts | 21 +- .../collections/vault.component.ts | 2 +- .../organizations/collections/vault.module.ts | 4 +- libs/common/src/enums/feature-flag.enum.ts | 2 - 6 files changed, 9 insertions(+), 1530 deletions(-) delete mode 100644 apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html delete mode 100644 apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.ts diff --git a/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html b/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html deleted file mode 100644 index 326dc627e17..00000000000 --- a/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html +++ /dev/null @@ -1,121 +0,0 @@ -@if (organization) { - - - - -} - - - -
-
- -
-
- - - {{ "all" | i18n }} - - - - {{ "addAccess" | i18n }} - - - - {{ trashCleanupWarning }} - - - - - - {{ "noItemsInList" | i18n }} - - - - - -
- - {{ "loading" | i18n }} -
-
-
diff --git a/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.ts deleted file mode 100644 index fce2827c073..00000000000 --- a/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.ts +++ /dev/null @@ -1,1389 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import { ActivatedRoute, Params, Router } from "@angular/router"; -import { - BehaviorSubject, - combineLatest, - firstValueFrom, - lastValueFrom, - merge, - Observable, - Subject, -} from "rxjs"; -import { - concatMap, - debounceTime, - distinctUntilChanged, - filter, - first, - map, - shareReplay, - switchMap, - takeUntil, - tap, -} from "rxjs/operators"; - -import { - CollectionAdminService, - CollectionAdminView, - CollectionService, - CollectionView, - Unassigned, -} from "@bitwarden/admin-console/common"; -import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; -import { NoResults } from "@bitwarden/assets/svg"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; -import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SyncService } from "@bitwarden/common/platform/sync"; -import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; -import { - BannerModule, - DialogRef, - DialogService, - NoItemsModule, - ToastService, -} from "@bitwarden/components"; -import { - AttachmentDialogResult, - AttachmentsV2Component, - CipherFormConfig, - CipherFormConfigService, - CollectionAssignmentResult, - DecryptionFailureDialogComponent, - PasswordRepromptService, -} from "@bitwarden/vault"; -import { - OrganizationFreeTrialWarningComponent, - OrganizationResellerRenewalWarningComponent, -} from "@bitwarden/web-vault/app/billing/organizations/warnings/components"; -import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; -import { VaultItemsComponent } from "@bitwarden/web-vault/app/vault/components/vault-items/vault-items.component"; - -import { SharedModule } from "../../../shared"; -import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections"; -import { - VaultItemDialogComponent, - VaultItemDialogMode, - VaultItemDialogResult, -} from "../../../vault/components/vault-item-dialog/vault-item-dialog.component"; -import { VaultItemEvent } from "../../../vault/components/vault-items/vault-item-event"; -import { VaultItemsModule } from "../../../vault/components/vault-items/vault-items.module"; -import { - BulkDeleteDialogResult, - openBulkDeleteDialog, -} from "../../../vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component"; -import { VaultFilterService } from "../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; -import { RoutedVaultFilterBridgeService } from "../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; -import { RoutedVaultFilterService } from "../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; -import { createFilterFunction } from "../../../vault/individual-vault/vault-filter/shared/models/filter-function"; -import { - All, - RoutedVaultFilterModel, -} from "../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; -import { VaultFilter } from "../../../vault/individual-vault/vault-filter/shared/models/vault-filter.model"; -import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service"; -import { GroupApiService, GroupView } from "../core"; -import { openEntityEventsDialog } from "../manage/entity-events.component"; -import { - CollectionDialogAction, - CollectionDialogTabType, - openCollectionDialog, -} from "../shared/components/collection-dialog"; - -import { - BulkCollectionsDialogComponent, - BulkCollectionsDialogResult, -} from "./bulk-collections-dialog"; -import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component"; -import { getFlatCollectionTree, getNestedCollectionTree } from "./utils"; -import { VaultFilterModule } from "./vault-filter/vault-filter.module"; -import { VaultHeaderComponent } from "./vault-header/vault-header.component"; - -const BroadcasterSubscriptionId = "OrgVaultComponent"; -const SearchTextDebounceInterval = 200; - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -enum AddAccessStatusType { - All = 0, - AddAccess = 1, -} - -@Component({ - selector: "app-org-vault", - templateUrl: "deprecated_vault.component.html", - imports: [ - VaultHeaderComponent, - CollectionAccessRestrictedComponent, - VaultFilterModule, - VaultItemsModule, - SharedModule, - BannerModule, - NoItemsModule, - OrganizationFreeTrialWarningComponent, - OrganizationResellerRenewalWarningComponent, - ], - providers: [ - RoutedVaultFilterService, - RoutedVaultFilterBridgeService, - { provide: CipherFormConfigService, useClass: AdminConsoleCipherFormConfigService }, - ], -}) -export class VaultComponent implements OnInit, OnDestroy { - protected Unassigned = Unassigned; - - trashCleanupWarning: string = null; - activeFilter: VaultFilter = new VaultFilter(); - - protected showAddAccessToggle = false; - protected noItemIcon = NoResults; - protected performingInitialLoad = true; - protected refreshing = false; - protected processingEvent = false; - protected filter: RoutedVaultFilterModel = {}; - protected organization: Organization; - protected allCollections: CollectionAdminView[]; - protected allGroups: GroupView[]; - protected ciphers: CipherView[]; - protected collections: CollectionAdminView[]; - protected selectedCollection: TreeNode | undefined; - protected isEmpty: boolean; - protected showCollectionAccessRestricted: boolean; - protected currentSearchText$: Observable; - protected prevCipherId: string | null = null; - protected userId: UserId; - /** - * A list of collections that the user can assign items to and edit those items within. - * @protected - */ - protected editableCollections$: Observable; - protected allCollectionsWithoutUnassigned$: Observable; - - protected get hideVaultFilters(): boolean { - return this.organization?.isProviderUser && !this.organization?.isMember; - } - - private searchText$ = new Subject(); - private refresh$ = new BehaviorSubject(null); - private destroy$ = new Subject(); - protected addAccessStatus$ = new BehaviorSubject(0); - private vaultItemDialogRef?: DialogRef | undefined; - - @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; - - constructor( - private route: ActivatedRoute, - private organizationService: OrganizationService, - protected vaultFilterService: VaultFilterService, - private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService, - private routedVaultFilterService: RoutedVaultFilterService, - private router: Router, - private changeDetectorRef: ChangeDetectorRef, - private syncService: SyncService, - private i18nService: I18nService, - private dialogService: DialogService, - private messagingService: MessagingService, - private broadcasterService: BroadcasterService, - private ngZone: NgZone, - private platformUtilsService: PlatformUtilsService, - private cipherService: CipherService, - private passwordRepromptService: PasswordRepromptService, - private collectionAdminService: CollectionAdminService, - private searchService: SearchService, - private searchPipe: SearchPipe, - private groupService: GroupApiService, - private logService: LogService, - private eventCollectionService: EventCollectionService, - private totpService: TotpService, - private apiService: ApiService, - private toastService: ToastService, - private configService: ConfigService, - private cipherFormConfigService: CipherFormConfigService, - protected billingApiService: BillingApiServiceAbstraction, - private accountService: AccountService, - private organizationWarningsService: OrganizationWarningsService, - private collectionService: CollectionService, - ) {} - - async ngOnInit() { - this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - - this.trashCleanupWarning = this.i18nService.t( - this.platformUtilsService.isSelfHost() - ? "trashCleanupWarningSelfHosted" - : "trashCleanupWarning", - ); - - const filter$ = this.routedVaultFilterService.filter$; - - // FIXME: The RoutedVaultFilterModel uses `organizationId: Unassigned` to represent the individual vault, - // but that is never used in Admin Console. This function narrows the type so it doesn't pollute our code here, - // but really we should change to using our own vault filter model that only represents valid states in AC. - const isOrganizationId = (value: OrganizationId | Unassigned): value is OrganizationId => - value !== Unassigned; - const organizationId$ = filter$.pipe( - map((filter) => filter.organizationId), - filter((filter) => filter !== undefined), - filter(isOrganizationId), - distinctUntilChanged(), - ); - - const organization$ = this.accountService.activeAccount$.pipe( - map((account) => account?.id), - switchMap((id) => - organizationId$.pipe( - switchMap((organizationId) => - this.organizationService - .organizations$(id) - .pipe(map((organizations) => organizations.find((org) => org.id === organizationId))), - ), - takeUntil(this.destroy$), - shareReplay({ refCount: false, bufferSize: 1 }), - ), - ), - ); - - const firstSetup$ = combineLatest([organization$, this.route.queryParams]).pipe( - first(), - switchMap(async ([organization]) => { - this.organization = organization; - - if (!organization.canEditAnyCollection) { - await this.syncService.fullSync(false); - } - - return undefined; - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - // 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.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - if (message.successfully) { - this.refresh(); - this.changeDetectorRef.detectChanges(); - } - break; - } - }); - }); - - this.routedVaultFilterBridgeService.activeFilter$ - .pipe(takeUntil(this.destroy$)) - .subscribe((activeFilter) => { - this.activeFilter = activeFilter; - - // watch the active filters. Only show toggle when viewing the collections filter - if (!this.activeFilter.collectionId) { - this.showAddAccessToggle = false; - } - }); - - this.searchText$ - .pipe(debounceTime(SearchTextDebounceInterval), takeUntil(this.destroy$)) - .subscribe((searchText) => - this.router.navigate([], { - queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText }, - queryParamsHandling: "merge", - replaceUrl: true, - }), - ); - - this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search)); - - this.allCollectionsWithoutUnassigned$ = this.refresh$.pipe( - switchMap(() => organizationId$), - switchMap((orgId) => - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.collectionAdminService.collectionAdminViews$(orgId, userId)), - ), - ), - shareReplay({ refCount: false, bufferSize: 1 }), - ); - - this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe( - map((collections) => { - // Users that can edit all ciphers can implicitly add to / edit within any collection - if (this.organization.canEditAllCiphers) { - return collections; - } - return collections.filter((c) => c.assigned); - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const allCollections$ = combineLatest([ - organizationId$, - this.allCollectionsWithoutUnassigned$, - ]).pipe( - map(([organizationId, allCollections]) => { - // FIXME: We should not assert that the Unassigned type is a CollectionId. - // Instead we should consider representing the Unassigned collection as a different object, given that - // it is not actually a collection. - return allCollections.concat( - new CollectionAdminView({ - name: this.i18nService.t("unassigned"), - id: Unassigned as CollectionId, - organizationId, - }), - ); - }), - ); - - const allGroups$ = organizationId$.pipe( - switchMap((organizationId) => this.groupService.getAll(organizationId)), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const allCiphers$ = combineLatest([organization$, this.refresh$]).pipe( - switchMap(async ([organization]) => { - // If user swaps organization reset the addAccessToggle - if (!this.showAddAccessToggle || organization) { - this.addAccessToggle(0); - } - let ciphers; - - // Restricted providers (who are not members) do not have access org cipher endpoint below - // Return early to avoid 404 response - if (!organization.isMember && organization.isProviderUser) { - return []; - } - - // If the user can edit all ciphers for the organization then fetch them ALL. - if (organization.canEditAllCiphers) { - ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id); - ciphers?.forEach((c) => (c.edit = true)); - } else { - // Otherwise, only fetch ciphers they have access to (includes unassigned for admins). - ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id); - } - - await this.searchService.indexCiphers(this.userId, ciphers, organization.id); - return ciphers; - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const allCipherMap$ = allCiphers$.pipe( - map((ciphers) => { - return Object.fromEntries(ciphers.map((c) => [c.id, c])); - }), - ); - - const nestedCollections$ = allCollections$.pipe( - map((collections) => getNestedCollectionTree(collections)), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const collections$ = combineLatest([ - nestedCollections$, - filter$, - this.currentSearchText$, - this.addAccessStatus$, - ]).pipe( - filter(([collections, filter]) => collections != undefined && filter != undefined), - concatMap(async ([collections, filter, searchText, addAccessStatus]) => { - if ( - filter.collectionId === Unassigned || - (filter.collectionId === undefined && filter.type !== undefined) - ) { - return []; - } - - this.showAddAccessToggle = false; - let searchableCollectionNodes: TreeNode[] = []; - if (filter.collectionId === undefined || filter.collectionId === All) { - searchableCollectionNodes = collections; - } else { - const selectedCollection = ServiceUtils.getTreeNodeObjectFromList( - collections, - filter.collectionId, - ); - searchableCollectionNodes = selectedCollection?.children ?? []; - } - - let collectionsToReturn: CollectionAdminView[] = []; - - if (await this.searchService.isSearchable(this.userId, searchText)) { - // Flatten the tree for searching through all levels - const flatCollectionTree: CollectionAdminView[] = - getFlatCollectionTree(searchableCollectionNodes); - - collectionsToReturn = this.searchPipe.transform( - flatCollectionTree, - searchText, - (collection) => collection.name, - (collection) => collection.id, - ); - } else { - collectionsToReturn = searchableCollectionNodes.map( - (treeNode: TreeNode): CollectionAdminView => treeNode.node, - ); - } - - // Add access toggle is only shown if allowAdminAccessToAllCollectionItems is false and there are unmanaged collections the user can edit - this.showAddAccessToggle = - !this.organization.allowAdminAccessToAllCollectionItems && - this.organization.canEditUnmanagedCollections && - collectionsToReturn.some((c) => c.unmanaged); - - if (addAccessStatus === 1 && this.showAddAccessToggle) { - collectionsToReturn = collectionsToReturn.filter((c) => c.unmanaged); - } - return collectionsToReturn; - }), - takeUntil(this.destroy$), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const selectedCollection$ = combineLatest([nestedCollections$, filter$]).pipe( - filter(([collections, filter]) => collections != undefined && filter != undefined), - map(([collections, filter]) => { - if ( - filter.collectionId === undefined || - filter.collectionId === All || - filter.collectionId === Unassigned - ) { - return undefined; - } - - return ServiceUtils.getTreeNodeObjectFromList(collections, filter.collectionId); - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const showCollectionAccessRestricted$ = combineLatest([ - filter$, - selectedCollection$, - organization$, - ]).pipe( - map(([filter, collection, organization]) => { - return ( - (filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers) || - (!organization.canEditAllCiphers && collection != undefined && !collection.node.assigned) - ); - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const ciphers$ = combineLatest([ - allCiphers$, - filter$, - this.currentSearchText$, - showCollectionAccessRestricted$, - ]).pipe( - filter(([ciphers, filter]) => ciphers != undefined && filter != undefined), - concatMap(async ([ciphers, filter, searchText, showCollectionAccessRestricted]) => { - if (filter.collectionId === undefined && filter.type === undefined) { - return []; - } - - if (showCollectionAccessRestricted) { - // Do not show ciphers for restricted collections - // Ciphers belonging to multiple collections may still be present in $allCiphers and shouldn't be visible - return []; - } - - const filterFunction = createFilterFunction(filter); - - if (await this.searchService.isSearchable(this.userId, searchText)) { - return await this.searchService.searchCiphers( - this.userId, - searchText, - [filterFunction], - ciphers, - ); - } - - return ciphers.filter(filterFunction); - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - firstSetup$ - .pipe( - switchMap(() => combineLatest([this.route.queryParams, allCipherMap$])), - filter(() => this.vaultItemDialogRef == undefined), - switchMap(async ([qParams, allCiphersMap]) => { - const cipherId = getCipherIdFromParams(qParams); - - if (!cipherId) { - this.prevCipherId = null; - return; - } - - if (cipherId === this.prevCipherId) { - return; - } - - this.prevCipherId = cipherId; - - const cipher = allCiphersMap[cipherId]; - if (cipher) { - let action = qParams.action; - - if (action == "showFailedToDecrypt") { - DecryptionFailureDialogComponent.open(this.dialogService, { - cipherIds: [cipherId as CipherId], - }); - await this.router.navigate([], { - queryParams: { itemId: null, cipherId: null, action: null }, - queryParamsHandling: "merge", - replaceUrl: true, - }); - return; - } - - // Default to "view" - if (action == null) { - action = "view"; - } - - if (action === "view") { - await this.viewCipherById(cipher); - } else { - await this.editCipher(cipher, false); - } - } else { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unknownCipher"), - }); - await this.router.navigate([], { - queryParams: { cipherId: null, itemId: null }, - queryParamsHandling: "merge", - }); - } - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - firstSetup$ - .pipe( - switchMap(() => combineLatest([this.route.queryParams, organization$, allCiphers$])), - switchMap(async ([qParams, organization, allCiphers$]) => { - const cipherId = qParams.viewEvents; - if (!cipherId) { - return; - } - const cipher = allCiphers$.find((c) => c.id === cipherId); - if (organization.useEvents && cipher != undefined) { - await this.viewEvents(cipher); - } else { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unknownCipher"), - }); - await this.router.navigate([], { - queryParams: { viewEvents: null }, - queryParamsHandling: "merge", - }); - } - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - // Billing Warnings - organization$ - .pipe( - switchMap((organization) => - merge( - this.organizationWarningsService.showInactiveSubscriptionDialog$(organization), - this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), - ), - ), - takeUntil(this.destroy$), - ) - .subscribe(); - // End Billing Warnings - - firstSetup$ - .pipe( - switchMap(() => this.refresh$), - tap(() => (this.refreshing = true)), - switchMap(() => - combineLatest([ - organization$, - filter$, - allCollections$, - allGroups$, - ciphers$, - collections$, - selectedCollection$, - showCollectionAccessRestricted$, - ]), - ), - takeUntil(this.destroy$), - ) - .subscribe( - ([ - organization, - filter, - allCollections, - allGroups, - ciphers, - collections, - selectedCollection, - showCollectionAccessRestricted, - ]) => { - this.organization = organization; - this.filter = filter; - this.allCollections = allCollections; - this.allGroups = allGroups; - this.ciphers = ciphers; - this.collections = collections; - this.selectedCollection = selectedCollection; - this.showCollectionAccessRestricted = showCollectionAccessRestricted; - - this.isEmpty = collections?.length === 0 && ciphers?.length === 0; - - // This is a temporary fix to avoid double fetching collections. - // TODO: Remove when implementing new VVR menu - this.vaultFilterService.reloadCollections(allCollections); - - this.refreshing = false; - this.performingInitialLoad = false; - }, - ); - } - - async navigateToPaymentMethod() { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; - await this.router.navigate(["organizations", `${this.organization?.id}`, "billing", route], { - state: { launchPaymentModalAutomatically: true }, - }); - } - - addAccessToggle(e: AddAccessStatusType) { - this.addAccessStatus$.next(e); - } - - get loading() { - return this.refreshing || this.processingEvent; - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - this.destroy$.next(); - this.destroy$.complete(); - } - - async onVaultItemsEvent(event: VaultItemEvent) { - this.processingEvent = true; - - try { - switch (event.type) { - case "viewAttachments": - await this.editCipherAttachments(event.item); - break; - case "clone": - await this.cloneCipher(event.item); - break; - case "restore": - if (event.items.length === 1) { - await this.restore(event.items[0]); - } else { - await this.bulkRestore(event.items); - } - break; - case "delete": { - const ciphers = event.items - .filter((i) => i.collection === undefined) - .map((i) => i.cipher); - const collections = event.items - .filter((i) => i.cipher === undefined) - .map((i) => i.collection); - if (ciphers.length === 1 && collections.length === 0) { - await this.deleteCipher(ciphers[0]); - } else if (ciphers.length === 0 && collections.length === 1) { - await this.deleteCollection(collections[0] as CollectionAdminView); - } else { - await this.bulkDelete(ciphers, collections, this.organization); - } - break; - } - case "copyField": - await this.copy(event.item, event.field); - break; - case "editCollection": - await this.editCollection( - event.item as CollectionAdminView, - CollectionDialogTabType.Info, - event.readonly, - ); - break; - case "viewCollectionAccess": - await this.editCollection( - event.item as CollectionAdminView, - CollectionDialogTabType.Access, - event.readonly, - ); - break; - case "bulkEditCollectionAccess": - await this.bulkEditCollectionAccess(event.items, this.organization); - break; - case "assignToCollections": - await this.bulkAssignToCollections(event.items); - break; - case "viewEvents": - await this.viewEvents(event.item); - break; - } - } finally { - this.processingEvent = false; - } - } - - filterSearchText(searchText: string) { - this.searchText$.next(searchText); - } - - async editCipherAttachments(cipher: CipherView) { - if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - this.go({ cipherId: null, itemId: null }); - return; - } - - if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) { - this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId }); - return; - } - - const dialogRef = AttachmentsV2Component.open(this.dialogService, { - cipherId: cipher.id as CipherId, - organizationId: cipher.organizationId as OrganizationId, - admin: true, - }); - - const result = await firstValueFrom(dialogRef.closed); - - if ( - result.action === AttachmentDialogResult.Removed || - result.action === AttachmentDialogResult.Uploaded - ) { - this.refresh(); - } - } - - /** Opens the Add/Edit Dialog */ - async addCipher(cipherType?: CipherType) { - const cipherFormConfig = await this.cipherFormConfigService.buildConfig( - "add", - null, - cipherType, - ); - - const collectionId: CollectionId | undefined = this.activeFilter.collectionId as CollectionId; - - cipherFormConfig.initialValues = { - organizationId: this.organization.id as OrganizationId, - collectionIds: collectionId ? [collectionId] : [], - }; - - await this.openVaultItemDialog("form", cipherFormConfig); - } - - /** - * Edit the given cipher or add a new cipher - * @param cipherView - When set, the cipher to be edited - * @param cloneCipher - `true` when the cipher should be cloned. - */ - async editCipher(cipher: CipherView | null, cloneCipher: boolean) { - if ( - cipher && - cipher.reprompt !== 0 && - !(await this.passwordRepromptService.showPasswordPrompt()) - ) { - // didn't pass password prompt, so don't open add / edit modal - this.go({ cipherId: null, itemId: null }); - return; - } - - const cipherFormConfig = await this.cipherFormConfigService.buildConfig( - cloneCipher ? "clone" : "edit", - cipher?.id as CipherId | null, - ); - - await this.openVaultItemDialog("form", cipherFormConfig, cipher); - } - - /** Opens the view dialog for the given cipher unless password reprompt fails */ - async viewCipherById(cipher: CipherView) { - if (!cipher) { - return; - } - - if ( - cipher && - cipher.reprompt !== 0 && - !(await this.passwordRepromptService.showPasswordPrompt()) - ) { - // Didn't pass password prompt, so don't open add / edit modal. - await this.go({ cipherId: null, itemId: null, action: null }); - return; - } - - const cipherFormConfig = await this.cipherFormConfigService.buildConfig( - "edit", - cipher.id as CipherId, - cipher.type, - ); - - await this.openVaultItemDialog( - "view", - cipherFormConfig, - cipher, - this.activeFilter.collectionId as CollectionId, - ); - } - - /** - * Open the combined view / edit dialog for a cipher. - */ - async openVaultItemDialog( - mode: VaultItemDialogMode, - formConfig: CipherFormConfig, - cipher?: CipherView, - activeCollectionId?: CollectionId, - ) { - const disableForm = cipher ? !cipher.edit && !this.organization.canEditAllCiphers : false; - // If the form is disabled, force the mode into `view` - const dialogMode = disableForm ? "view" : mode; - this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, { - mode: dialogMode, - formConfig, - disableForm, - activeCollectionId, - isAdminConsoleAction: true, - restore: this.restore, - }); - - const result = await lastValueFrom(this.vaultItemDialogRef.closed); - this.vaultItemDialogRef = undefined; - - // If the dialog was closed by deleting the cipher, refresh the vault. - if (result === VaultItemDialogResult.Deleted || result === VaultItemDialogResult.Saved) { - this.refresh(); - } - - // Clear the query params when the dialog closes - await this.go({ cipherId: null, itemId: null, action: null }); - } - - async cloneCipher(cipher: CipherView) { - if (cipher.login?.hasFido2Credentials) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "passkeyNotCopied" }, - content: { key: "passkeyNotCopiedAlert" }, - type: "info", - }); - - if (!confirmed) { - return false; - } - } - - await this.editCipher(cipher, true); - } - - restore = async (c: CipherView): Promise => { - if (!c.isDeleted) { - return; - } - - if ( - !this.organization.permissions.editAnyCollection && - !c.edit && - !this.organization.allowAdminAccessToAllCollectionItems - ) { - this.showMissingPermissionsError(); - return; - } - - if (!(await this.repromptCipher([c]))) { - return; - } - - // Allow restore of an Unassigned Item - try { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const asAdmin = this.organization?.canEditAnyCollection || c.isUnassigned; - await this.cipherService.restoreWithServer(c.id, activeUserId, asAdmin); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("restoredItem"), - }); - this.refresh(); - } catch (e) { - this.logService.error(e); - } - }; - - async bulkRestore(ciphers: CipherView[]) { - if ( - !this.organization.permissions.editAnyCollection && - ciphers.some((c) => !c.edit && !this.organization.allowAdminAccessToAllCollectionItems) - ) { - this.showMissingPermissionsError(); - return; - } - - if (!(await this.repromptCipher(ciphers))) { - return; - } - - // assess if there are unassigned ciphers and/or editable ciphers selected in bulk for restore - const editAccessCiphers: string[] = []; - const unassignedCiphers: string[] = []; - - // If user has edit all Access no need to check for unassigned ciphers - if (this.organization.canEditAllCiphers) { - ciphers.map((cipher) => { - editAccessCiphers.push(cipher.id); - }); - } else { - ciphers.map((cipher) => { - if (cipher.collectionIds.length === 0) { - unassignedCiphers.push(cipher.id); - } else if (cipher.edit) { - editAccessCiphers.push(cipher.id); - } - }); - } - - if (unassignedCiphers.length === 0 && editAccessCiphers.length === 0) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("nothingSelected"), - }); - return; - } - - if (unassignedCiphers.length > 0 || editAccessCiphers.length > 0) { - await this.cipherService.restoreManyWithServer( - [...unassignedCiphers, ...editAccessCiphers], - this.userId, - this.organization.id, - ); - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("restoredItems"), - }); - this.refresh(); - } - - async deleteCipher(c: CipherView): Promise { - if (!c.edit && !this.organization.canEditAllCiphers) { - this.showMissingPermissionsError(); - return; - } - - if (!(await this.repromptCipher([c]))) { - return; - } - - const permanent = c.isDeleted; - - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: permanent ? "permanentlyDeleteItem" : "deleteItem" }, - content: { key: permanent ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - try { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - await this.deleteCipherWithServer(c.id, activeUserId, permanent, c.isUnassigned); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"), - }); - this.refresh(); - } catch (e) { - this.logService.error(e); - } - } - - async deleteCollection(collection: CollectionAdminView): Promise { - if (!collection.canDelete(this.organization)) { - this.showMissingPermissionsError(); - return; - } - const confirmed = await this.dialogService.openSimpleDialog({ - title: collection.name, - content: { key: "deleteCollectionConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - try { - await this.apiService.deleteCollection(this.organization?.id, collection.id); - await this.collectionService.delete([collection.id as CollectionId], this.userId); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("deletedCollectionId", collection.name), - }); - - // Clear the cipher cache to clear the deleted collection from the cipher state - await this.cipherService.clear(); - - // Navigate away if we deleted the collection we were viewing - if (this.selectedCollection?.node.id === collection.id) { - void this.router.navigate([], { - queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, - queryParamsHandling: "merge", - replaceUrl: true, - }); - } - - this.refresh(); - } catch (e) { - this.logService.error(e); - } - } - - async bulkDelete( - ciphers: CipherView[], - collections: CollectionView[], - organization: Organization, - ) { - if (!(await this.repromptCipher(ciphers))) { - return; - } - - // Allow bulk deleting of Unassigned Items - const unassignedCiphers: string[] = []; - const assignedCiphers: string[] = []; - - ciphers.map((c) => { - if (c.isUnassigned) { - unassignedCiphers.push(c.id); - } else { - assignedCiphers.push(c.id); - } - }); - - if (ciphers.length === 0 && collections.length === 0) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("nothingSelected"), - }); - return; - } - - const canDeleteCollections = - collections == null || collections.every((c) => c.canDelete(organization)); - const canDeleteCiphers = - ciphers == null || ciphers.every((c) => c.edit) || this.organization.canEditAllCiphers; - - if (!canDeleteCiphers || !canDeleteCollections) { - this.showMissingPermissionsError(); - return; - } - - const dialog = openBulkDeleteDialog(this.dialogService, { - data: { - permanent: this.filter.type === "trash", - cipherIds: assignedCiphers, - collections: collections, - organization, - unassignedCiphers, - }, - }); - - const result = await lastValueFrom(dialog.closed); - if (result === BulkDeleteDialogResult.Deleted) { - this.refresh(); - } - } - - async copy(cipher: CipherView, field: "username" | "password" | "totp") { - let aType; - let value; - let typeI18nKey; - - if (field === "username") { - aType = "Username"; - value = cipher.login.username; - typeI18nKey = "username"; - } else if (field === "password") { - aType = "Password"; - value = cipher.login.password; - typeI18nKey = "password"; - } else if (field === "totp") { - aType = "TOTP"; - const totpResponse = await firstValueFrom(this.totpService.getCode$(cipher.login.totp)); - value = totpResponse?.code; - typeI18nKey = "verificationCodeTotp"; - } else { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unexpectedError"), - }); - return; - } - - if ( - this.passwordRepromptService.protectedFields().includes(aType) && - !(await this.repromptCipher([cipher])) - ) { - return; - } - - if (!cipher.viewPassword) { - return; - } - - this.platformUtilsService.copyToClipboard(value, { window: window }); - this.toastService.showToast({ - variant: "info", - title: null, - message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - }); - - if (field === "password") { - await this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id); - } else if (field === "totp") { - await this.eventCollectionService.collect( - EventType.Cipher_ClientCopiedHiddenField, - cipher.id, - ); - } - } - - async addCollection(): Promise { - const dialog = openCollectionDialog(this.dialogService, { - data: { - organizationId: this.organization?.id, - parentCollectionId: this.selectedCollection?.node.id, - limitNestedCollections: !this.organization.canEditAnyCollection, - isAdminConsoleActive: true, - }, - }); - - const result = await lastValueFrom(dialog.closed); - if ( - result.action === CollectionDialogAction.Saved || - result.action === CollectionDialogAction.Deleted - ) { - this.refresh(); - } - } - - async editCollection( - c: CollectionAdminView, - tab: CollectionDialogTabType, - readonly: boolean, - ): Promise { - const dialog = openCollectionDialog(this.dialogService, { - data: { - collectionId: c?.id, - organizationId: this.organization?.id, - initialTab: tab, - readonly: readonly, - isAddAccessCollection: c.unmanaged, - limitNestedCollections: !this.organization.canEditAnyCollection, - isAdminConsoleActive: true, - }, - }); - - const result = await lastValueFrom(dialog.closed); - if ( - result.action === CollectionDialogAction.Saved || - result.action === CollectionDialogAction.Deleted - ) { - this.refresh(); - - // If we deleted the selected collection, navigate up/away - if ( - result.action === CollectionDialogAction.Deleted && - this.selectedCollection?.node.id === c?.id - ) { - void this.router.navigate([], { - queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, - queryParamsHandling: "merge", - replaceUrl: true, - }); - } - } - } - - async bulkEditCollectionAccess( - collections: CollectionView[], - organization: Organization, - ): Promise { - if (collections.length === 0) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("noCollectionsSelected"), - }); - return; - } - - if (collections.some((c) => !c.canEdit(organization))) { - this.showMissingPermissionsError(); - return; - } - - const dialog = BulkCollectionsDialogComponent.open(this.dialogService, { - data: { - collections, - organizationId: this.organization?.id, - }, - }); - - const result = await lastValueFrom(dialog.closed); - if (result === BulkCollectionsDialogResult.Saved) { - this.refresh(); - } - } - - async bulkAssignToCollections(items: CipherView[]) { - if (items.length === 0) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("nothingSelected"), - }); - return; - } - - const availableCollections = await firstValueFrom(this.editableCollections$); - - const dialog = AssignCollectionsWebComponent.open(this.dialogService, { - data: { - ciphers: items, - organizationId: this.organization?.id as OrganizationId, - availableCollections, - activeCollection: this.activeFilter?.selectedCollectionNode?.node, - isSingleCipherAdmin: - items.length === 1 && (this.organization?.canEditAllCiphers || items[0].isUnassigned), - }, - }); - - const result = await lastValueFrom(dialog.closed); - if (result === CollectionAssignmentResult.Saved) { - this.refresh(); - } - } - - async viewEvents(cipher: CipherView) { - await openEntityEventsDialog(this.dialogService, { - data: { - name: cipher.name, - organizationId: this.organization.id, - entityId: cipher.id, - showUser: true, - entity: "cipher", - }, - }); - } - - protected deleteCipherWithServer( - id: string, - userId: UserId, - permanent: boolean, - isUnassigned: boolean, - ) { - const asAdmin = this.organization?.canEditAllCiphers || isUnassigned; - return permanent - ? this.cipherService.deleteWithServer(id, userId, asAdmin) - : this.cipherService.softDeleteWithServer(id, userId, asAdmin); - } - - protected async repromptCipher(ciphers: CipherView[]) { - const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None); - - return notProtected || (await this.passwordRepromptService.showPasswordPrompt()); - } - - private refresh() { - this.refresh$.next(); - this.vaultItemsComponent?.clearSelection(); - } - - private go(queryParams: any = null) { - if (queryParams == null) { - queryParams = { - type: this.activeFilter.cipherType, - collectionId: this.activeFilter.collectionId, - deleted: this.activeFilter.isDeleted || null, - }; - } - - void this.router.navigate([], { - relativeTo: this.route, - queryParams: queryParams, - queryParamsHandling: "merge", - replaceUrl: true, - }); - } - - protected readonly CollectionDialogTabType = CollectionDialogTabType; - - private showMissingPermissionsError() { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("missingPermissions"), - }); - } -} - -/** - * Allows backwards compatibility with - * old links that used the original `cipherId` param - */ -const getCipherIdFromParams = (params: Params): string => { - return params["itemId"] || params["cipherId"]; -}; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-routing.module.ts b/apps/web/src/app/admin-console/organizations/collections/vault-routing.module.ts index d529c4c31fe..7ad9f050d7b 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-routing.module.ts @@ -1,26 +1,19 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { canAccessVaultTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; -import { VaultComponent } from "./deprecated_vault.component"; -import { vNextVaultComponent } from "./vault.component"; +import { VaultComponent } from "./vault.component"; const routes: Routes = [ - ...featureFlaggedRoute({ - defaultComponent: VaultComponent, - flaggedComponent: vNextVaultComponent, - featureFlag: FeatureFlag.CollectionVaultRefactor, - routeOptions: { - data: { titleId: "vaults" }, - path: "", - canActivate: [organizationPermissionsGuard(canAccessVaultTab)], - }, - }), + { + data: { titleId: "vaults" }, + path: "", + canActivate: [organizationPermissionsGuard(canAccessVaultTab)], + component: VaultComponent, + }, ]; @NgModule({ diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 64aa6936468..51315b9a1a5 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -162,7 +162,7 @@ enum AddAccessStatusType { { provide: CipherFormConfigService, useClass: AdminConsoleCipherFormConfigService }, ], }) -export class vNextVaultComponent implements OnInit, OnDestroy { +export class VaultComponent implements OnInit, OnDestroy { protected Unassigned = Unassigned; trashCleanupWarning: string = this.i18nService.t( diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.module.ts b/apps/web/src/app/admin-console/organizations/collections/vault.module.ts index 92dbc5d832c..1a093ff8352 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.module.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.module.ts @@ -6,10 +6,9 @@ import { ViewComponent } from "../../../vault/individual-vault/view.component"; import { CollectionDialogComponent } from "../shared/components/collection-dialog"; import { CollectionNameBadgeComponent } from "./collection-badge"; -import { VaultComponent } from "./deprecated_vault.component"; import { GroupBadgeModule } from "./group-badge/group-badge.module"; import { VaultRoutingModule } from "./vault-routing.module"; -import { vNextVaultComponent } from "./vault.component"; +import { VaultComponent } from "./vault.component"; @NgModule({ imports: [ @@ -20,7 +19,6 @@ import { vNextVaultComponent } from "./vault.component"; OrganizationBadgeModule, CollectionDialogComponent, VaultComponent, - vNextVaultComponent, ViewComponent, ], }) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index bd874f934f0..67836befd7c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -12,7 +12,6 @@ import { ServerConfig } from "../platform/abstractions/config/server-config"; export enum FeatureFlag { /* Admin Console Team */ CreateDefaultLocation = "pm-19467-create-default-location", - CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors", /* Auth */ PM22110_DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods", @@ -74,7 +73,6 @@ const FALSE = false as boolean; export const DefaultFeatureFlagValue = { /* Admin Console Team */ [FeatureFlag.CreateDefaultLocation]: FALSE, - [FeatureFlag.CollectionVaultRefactor]: FALSE, /* Autofill */ [FeatureFlag.MacOsNativeCredentialSync]: FALSE, From dafbe7db1fd3602287d08f6cdae61f846f840d19 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Tue, 30 Sep 2025 11:59:19 -0400 Subject: [PATCH 45/55] add bold font to dt and add margin to dl (#16649) --- libs/components/src/tw-theme-preflight.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libs/components/src/tw-theme-preflight.css b/libs/components/src/tw-theme-preflight.css index e5f35885993..372c80e0881 100644 --- a/libs/components/src/tw-theme-preflight.css +++ b/libs/components/src/tw-theme-preflight.css @@ -54,6 +54,14 @@ display: none !important; } + dl { + @apply tw-mb-4; + } + + dt { + @apply tw-font-bold; + } + hr { border-color: rgba(0, 0, 0, 0.1); } From 6499ecb6ee9dcb81d29e9b3adf35c844f64cc0a7 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 30 Sep 2025 11:55:27 -0500 Subject: [PATCH 46/55] PM-26329 bug fixes to the cards (#16665) --- apps/web/src/locales/en/messages.json | 8 ++++---- .../dirt/access-intelligence/all-activity.component.html | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index e2bb463c939..2d70a79a7bf 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -59,8 +59,8 @@ "createNewLoginItem": { "message": "Create new login item" }, - "onceYouMarkCriticalApplicationsActivityDescription": { - "message": "Once you mark applications critical, they will display here." + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "Once you mark applications critical, they will display here" }, "viewAtRiskMembers": { "message": "View at-risk members" @@ -165,8 +165,8 @@ "membersAtRiskActivityDescription":{ "message": "Members with edit access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html index 6598d197172..8d564502ee4 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html @@ -22,7 +22,7 @@ Date: Tue, 30 Sep 2025 13:04:22 -0400 Subject: [PATCH 47/55] add back missing chevron (#16614) * add back missing chevron * add transform origin to center chevron correctly * update top position to center chevron properly --- libs/components/src/form-field/form-field.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/components/src/form-field/form-field.component.html b/libs/components/src/form-field/form-field.component.html index ae3bad40698..c2c92104727 100644 --- a/libs/components/src/form-field/form-field.component.html +++ b/libs/components/src/form-field/form-field.component.html @@ -56,7 +56,7 @@
- -
{{ "noItemsInArchive" | i18n }}
-

- {{ "archivedItemsDescription" | i18n }} + +

+ {{ (emptyState$ | async)?.title | i18n }} +
+

+ {{ (emptyState$ | async)?.description | i18n }}

-
{{ "noItemsInList" | i18n }}
- -
- - - - - - {{ "loading" | i18n }} - diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts deleted file mode 100644 index 431f8882505..00000000000 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ /dev/null @@ -1,360 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { - Component, - EventEmitter, - Input, - OnDestroy, - OnInit, - Output, - ViewChild, -} from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { firstValueFrom, from, Subject, switchMap, takeUntil } from "rxjs"; - -import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { - BillingInformation, - OrganizationBillingServiceAbstraction as OrganizationBillingService, - OrganizationInformation, - PaymentInformation, - PlanInformation, -} from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { - PaymentMethodType, - PlanType, - ProductTierType, - ProductType, -} from "@bitwarden/common/billing/enums"; -import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax"; -import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { ToastService } from "@bitwarden/components"; - -import { BillingSharedModule } from "../../shared"; -import { PaymentComponent } from "../../shared/payment/payment.component"; - -export type TrialOrganizationType = Exclude; - -export interface OrganizationInfo { - name: string; - email: string; - type: TrialOrganizationType | null; -} - -export interface OrganizationCreatedEvent { - organizationId: string; - planDescription: string; -} - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -enum SubscriptionCadence { - Annual, - Monthly, -} - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum SubscriptionProduct { - PasswordManager, - SecretsManager, -} - -@Component({ - selector: "app-trial-billing-step", - templateUrl: "trial-billing-step.component.html", - imports: [BillingSharedModule], -}) -export class TrialBillingStepComponent implements OnInit, OnDestroy { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent; - @Input() organizationInfo: OrganizationInfo; - @Input() subscriptionProduct: SubscriptionProduct = SubscriptionProduct.PasswordManager; - @Input() trialLength: number; - @Output() steppedBack = new EventEmitter(); - @Output() organizationCreated = new EventEmitter(); - - loading = true; - fetchingTaxAmount = false; - - annualCadence = SubscriptionCadence.Annual; - monthlyCadence = SubscriptionCadence.Monthly; - - formGroup = this.formBuilder.group({ - cadence: [SubscriptionCadence.Annual, Validators.required], - }); - formPromise: Promise; - - applicablePlans: PlanResponse[]; - annualPlan?: PlanResponse; - monthlyPlan?: PlanResponse; - - taxAmount = 0; - - private destroy$ = new Subject(); - - protected readonly SubscriptionProduct = SubscriptionProduct; - - constructor( - private apiService: ApiService, - private i18nService: I18nService, - private formBuilder: FormBuilder, - private messagingService: MessagingService, - private organizationBillingService: OrganizationBillingService, - private toastService: ToastService, - private taxService: TaxServiceAbstraction, - private accountService: AccountService, - ) {} - - async ngOnInit(): Promise { - const plans = await this.apiService.getPlans(); - this.applicablePlans = plans.data.filter(this.isApplicable); - this.annualPlan = this.findPlanFor(SubscriptionCadence.Annual); - this.monthlyPlan = this.findPlanFor(SubscriptionCadence.Monthly); - - if (this.trialLength === 0) { - this.formGroup.controls.cadence.valueChanges - .pipe( - switchMap((cadence) => from(this.previewTaxAmount(cadence))), - takeUntil(this.destroy$), - ) - .subscribe((taxAmount) => { - this.taxAmount = taxAmount; - }); - } - - this.loading = false; - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - async submit(): Promise { - if (!this.taxInfoComponent.validate()) { - return; - } - - this.formPromise = this.createOrganization(); - - const organizationId = await this.formPromise; - const planDescription = this.getPlanDescription(); - - this.toastService.showToast({ - variant: "success", - title: this.i18nService.t("organizationCreated"), - message: this.i18nService.t("organizationReadyToGo"), - }); - - this.organizationCreated.emit({ - organizationId, - planDescription, - }); - - // TODO: No one actually listening to this? - this.messagingService.send("organizationCreated", { organizationId }); - } - - async onTaxInformationChanged() { - if (this.trialLength === 0) { - this.taxAmount = await this.previewTaxAmount(this.formGroup.value.cadence); - } - - this.paymentComponent.showBankAccount = - this.taxInfoComponent.getTaxInformation().country === "US"; - if ( - !this.paymentComponent.showBankAccount && - this.paymentComponent.selected === PaymentMethodType.BankAccount - ) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - - protected getPriceFor(cadence: SubscriptionCadence): number { - const plan = this.findPlanFor(cadence); - return this.subscriptionProduct === SubscriptionProduct.PasswordManager - ? plan.PasswordManager.basePrice === 0 - ? plan.PasswordManager.seatPrice - : plan.PasswordManager.basePrice - : plan.SecretsManager.basePrice === 0 - ? plan.SecretsManager.seatPrice - : plan.SecretsManager.basePrice; - } - - protected stepBack() { - this.steppedBack.emit(); - } - - private async createOrganization(): Promise { - const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - const planResponse = this.findPlanFor(this.formGroup.value.cadence); - - const { type, token } = await this.paymentComponent.tokenize(); - const paymentMethod: [string, PaymentMethodType] = [token, type]; - - const organization: OrganizationInformation = { - name: this.organizationInfo.name, - billingEmail: this.organizationInfo.email, - initiationPath: - this.subscriptionProduct === SubscriptionProduct.PasswordManager - ? "Password Manager trial from marketing website" - : "Secrets Manager trial from marketing website", - }; - - const plan: PlanInformation = { - type: planResponse.type, - passwordManagerSeats: 1, - }; - - if (this.subscriptionProduct === SubscriptionProduct.SecretsManager) { - plan.subscribeToSecretsManager = true; - plan.isFromSecretsManagerTrial = true; - plan.secretsManagerSeats = 1; - } - - const payment: PaymentInformation = { - paymentMethod, - billing: this.getBillingInformationFromTaxInfoComponent(), - skipTrial: this.trialLength === 0, - }; - - const response = await this.organizationBillingService.purchaseSubscription( - { - organization, - plan, - payment, - }, - activeUserId, - ); - - return response.id; - } - - private productTypeToPlanTypeMap: { - [productType in TrialOrganizationType]: { - [cadence in SubscriptionCadence]?: PlanType; - }; - } = { - [ProductTierType.Enterprise]: { - [SubscriptionCadence.Annual]: PlanType.EnterpriseAnnually, - [SubscriptionCadence.Monthly]: PlanType.EnterpriseMonthly, - }, - [ProductTierType.Families]: { - [SubscriptionCadence.Annual]: PlanType.FamiliesAnnually, - // No monthly option for Families plan - }, - [ProductTierType.Teams]: { - [SubscriptionCadence.Annual]: PlanType.TeamsAnnually, - [SubscriptionCadence.Monthly]: PlanType.TeamsMonthly, - }, - [ProductTierType.TeamsStarter]: { - // No annual option for Teams Starter plan - [SubscriptionCadence.Monthly]: PlanType.TeamsStarter, - }, - }; - - private findPlanFor(cadence: SubscriptionCadence): PlanResponse | null { - const productType = this.organizationInfo.type; - const planType = this.productTypeToPlanTypeMap[productType]?.[cadence]; - return planType ? this.applicablePlans.find((plan) => plan.type === planType) : null; - } - - protected get showTaxIdField(): boolean { - switch (this.organizationInfo.type) { - case ProductTierType.Families: - return false; - default: - return true; - } - } - - private getBillingInformationFromTaxInfoComponent(): BillingInformation { - return { - postalCode: this.taxInfoComponent.getTaxInformation()?.postalCode, - country: this.taxInfoComponent.getTaxInformation()?.country, - taxId: this.taxInfoComponent.getTaxInformation()?.taxId, - addressLine1: this.taxInfoComponent.getTaxInformation()?.line1, - addressLine2: this.taxInfoComponent.getTaxInformation()?.line2, - city: this.taxInfoComponent.getTaxInformation()?.city, - state: this.taxInfoComponent.getTaxInformation()?.state, - }; - } - - private getPlanDescription(): string { - const plan = this.findPlanFor(this.formGroup.value.cadence); - const price = - this.subscriptionProduct === SubscriptionProduct.PasswordManager - ? plan.PasswordManager.basePrice === 0 - ? plan.PasswordManager.seatPrice - : plan.PasswordManager.basePrice - : plan.SecretsManager.basePrice === 0 - ? plan.SecretsManager.seatPrice - : plan.SecretsManager.basePrice; - - switch (this.formGroup.value.cadence) { - case SubscriptionCadence.Annual: - return `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`; - case SubscriptionCadence.Monthly: - return `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`; - } - } - - private isApplicable(plan: PlanResponse): boolean { - const hasCorrectProductType = - plan.productTier === ProductTierType.Enterprise || - plan.productTier === ProductTierType.Families || - plan.productTier === ProductTierType.Teams || - plan.productTier === ProductTierType.TeamsStarter; - const notDisabledOrLegacy = !plan.disabled && !plan.legacyYear; - return hasCorrectProductType && notDisabledOrLegacy; - } - - private previewTaxAmount = async (cadence: SubscriptionCadence): Promise => { - this.fetchingTaxAmount = true; - - if (!this.taxInfoComponent.validate()) { - this.fetchingTaxAmount = false; - return 0; - } - - const plan = this.findPlanFor(cadence); - - const productType = - this.subscriptionProduct === SubscriptionProduct.PasswordManager - ? ProductType.PasswordManager - : ProductType.SecretsManager; - - const taxInformation = this.taxInfoComponent.getTaxInformation(); - - const request: PreviewTaxAmountForOrganizationTrialRequest = { - planType: plan.type, - productType, - taxInformation: { - ...taxInformation, - }, - }; - - const response = await this.taxService.previewTaxAmountForOrganizationTrial(request); - this.fetchingTaxAmount = false; - return response; - }; - - get price() { - return this.getPriceFor(this.formGroup.value.cadence); - } - - get total() { - return this.price + this.taxAmount; - } - - get interval() { - return this.formGroup.value.cadence === SubscriptionCadence.Annual ? "year" : "month"; - } -} diff --git a/apps/web/src/app/billing/clients/index.ts b/apps/web/src/app/billing/clients/index.ts index ff962abcbf3..17f64248cfa 100644 --- a/apps/web/src/app/billing/clients/index.ts +++ b/apps/web/src/app/billing/clients/index.ts @@ -1,2 +1,3 @@ export * from "./organization-billing.client"; export * from "./subscriber-billing.client"; +export * from "./tax.client"; diff --git a/apps/web/src/app/billing/clients/subscriber-billing.client.ts b/apps/web/src/app/billing/clients/subscriber-billing.client.ts index 18ca215ef0c..107a8ccc728 100644 --- a/apps/web/src/app/billing/clients/subscriber-billing.client.ts +++ b/apps/web/src/app/billing/clients/subscriber-billing.client.ts @@ -82,6 +82,24 @@ export class SubscriberBillingClient { return data ? new MaskedPaymentMethodResponse(data).value : null; }; + restartSubscription = async ( + subscriber: BitwardenSubscriber, + paymentMethod: TokenizedPaymentMethod, + billingAddress: BillingAddress, + ): Promise => { + const path = `${this.getEndpoint(subscriber)}/subscription/restart`; + await this.apiService.send( + "POST", + path, + { + paymentMethod, + billingAddress, + }, + true, + false, + ); + }; + updateBillingAddress = async ( subscriber: BitwardenSubscriber, billingAddress: BillingAddress, diff --git a/apps/web/src/app/billing/clients/tax.client.ts b/apps/web/src/app/billing/clients/tax.client.ts new file mode 100644 index 00000000000..09debd5a210 --- /dev/null +++ b/apps/web/src/app/billing/clients/tax.client.ts @@ -0,0 +1,131 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BillingAddress } from "@bitwarden/web-vault/app/billing/payment/types"; + +class TaxAmountResponse extends BaseResponse implements TaxAmounts { + tax: number; + total: number; + + constructor(response: any) { + super(response); + + this.tax = this.getResponseProperty("Tax"); + this.total = this.getResponseProperty("Total"); + } +} + +export type OrganizationSubscriptionPlan = { + tier: "families" | "teams" | "enterprise"; + cadence: "annually" | "monthly"; +}; + +export type OrganizationSubscriptionPurchase = OrganizationSubscriptionPlan & { + passwordManager: { + seats: number; + additionalStorage: number; + sponsored: boolean; + }; + secretsManager?: { + seats: number; + additionalServiceAccounts: number; + standalone: boolean; + }; +}; + +export type OrganizationSubscriptionUpdate = { + passwordManager?: { + seats?: number; + additionalStorage?: number; + }; + secretsManager?: { + seats?: number; + additionalServiceAccounts?: number; + }; +}; + +export interface TaxAmounts { + tax: number; + total: number; +} + +@Injectable() +export class TaxClient { + constructor(private apiService: ApiService) {} + + previewTaxForOrganizationSubscriptionPurchase = async ( + purchase: OrganizationSubscriptionPurchase, + billingAddress: BillingAddress, + ): Promise => { + const json = await this.apiService.send( + "POST", + "/billing/tax/organizations/subscriptions/purchase", + { + purchase, + billingAddress, + }, + true, + true, + ); + + return new TaxAmountResponse(json); + }; + + previewTaxForOrganizationSubscriptionPlanChange = async ( + organizationId: string, + plan: { + tier: "families" | "teams" | "enterprise"; + cadence: "annually" | "monthly"; + }, + billingAddress: BillingAddress | null, + ): Promise => { + const json = await this.apiService.send( + "POST", + `/billing/tax/organizations/${organizationId}/subscription/plan-change`, + { + plan, + billingAddress, + }, + true, + true, + ); + + return new TaxAmountResponse(json); + }; + + previewTaxForOrganizationSubscriptionUpdate = async ( + organizationId: string, + update: OrganizationSubscriptionUpdate, + ): Promise => { + const json = await this.apiService.send( + "POST", + `/billing/tax/organizations/${organizationId}/subscription/update`, + { + update, + }, + true, + true, + ); + + return new TaxAmountResponse(json); + }; + + previewTaxForPremiumSubscriptionPurchase = async ( + additionalStorage: number, + billingAddress: BillingAddress, + ): Promise => { + const json = await this.apiService.send( + "POST", + `/billing/tax/premium/subscriptions/purchase`, + { + additionalStorage, + billingAddress, + }, + true, + true, + ); + + return new TaxAmountResponse(json); + }; +} diff --git a/apps/web/src/app/billing/index.ts b/apps/web/src/app/billing/index.ts index 217f1e05be9..a3047bbab6a 100644 --- a/apps/web/src/app/billing/index.ts +++ b/apps/web/src/app/billing/index.ts @@ -1,2 +1 @@ export { OrganizationPlansComponent } from "./organizations"; -export { TaxInfoComponent } from "./shared"; diff --git a/apps/web/src/app/billing/individual/index.ts b/apps/web/src/app/billing/individual/index.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts index 87b342ed997..bb0ca60b677 100644 --- a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts @@ -3,8 +3,6 @@ import { RouterModule, Routes } from "@angular/router"; import { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component"; -import { PaymentMethodComponent } from "../shared"; - import { BillingHistoryViewComponent } from "./billing-history-view.component"; import { PremiumComponent } from "./premium/premium.component"; import { SubscriptionComponent } from "./subscription.component"; @@ -27,11 +25,6 @@ const routes: Routes = [ component: PremiumComponent, data: { titleId: "goPremium" }, }, - { - path: "payment-method", - component: PaymentMethodComponent, - data: { titleId: "paymentMethod" }, - }, { path: "payment-details", component: AccountPaymentDetailsComponent, diff --git a/apps/web/src/app/billing/individual/individual-billing.module.ts b/apps/web/src/app/billing/individual/individual-billing.module.ts index ad75da00c99..20f2a6cc143 100644 --- a/apps/web/src/app/billing/individual/individual-billing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing.module.ts @@ -1,5 +1,10 @@ import { NgModule } from "@angular/core"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, +} from "@bitwarden/web-vault/app/billing/payment/components"; + import { HeaderModule } from "../../layouts/header/header.module"; import { BillingSharedModule } from "../shared"; @@ -10,7 +15,13 @@ import { SubscriptionComponent } from "./subscription.component"; import { UserSubscriptionComponent } from "./user-subscription.component"; @NgModule({ - imports: [IndividualBillingRoutingModule, BillingSharedModule, HeaderModule], + imports: [ + IndividualBillingRoutingModule, + BillingSharedModule, + HeaderModule, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, + ], declarations: [ SubscriptionComponent, BillingHistoryViewComponent, diff --git a/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts b/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts index 9f46d9d3909..ca7902542de 100644 --- a/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts +++ b/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts @@ -1,22 +1,7 @@ import { Component } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { - BehaviorSubject, - EMPTY, - filter, - from, - map, - merge, - Observable, - shareReplay, - switchMap, - tap, -} from "rxjs"; -import { catchError } from "rxjs/operators"; +import { BehaviorSubject, filter, merge, Observable, shareReplay, switchMap, tap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; @@ -28,13 +13,6 @@ import { import { MaskedPaymentMethod } from "../../payment/types"; import { mapAccountToSubscriber, BitwardenSubscriber } from "../../types"; -class RedirectError { - constructor( - public path: string[], - public relativeTo: ActivatedRoute, - ) {} -} - type View = { account: BitwardenSubscriber; paymentMethod: MaskedPaymentMethod | null; @@ -56,23 +34,11 @@ export class AccountPaymentDetailsComponent { private viewState$ = new BehaviorSubject(null); private load$: Observable = this.accountService.activeAccount$.pipe( - switchMap((account) => - this.configService - .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) - .pipe( - map((managePaymentDetailsOutsideCheckout) => { - if (!managePaymentDetailsOutsideCheckout) { - throw new RedirectError(["../payment-method"], this.activatedRoute); - } - return account; - }), - ), - ), mapAccountToSubscriber, switchMap(async (account) => { const [paymentMethod, credit] = await Promise.all([ - this.billingClient.getPaymentMethod(account), - this.billingClient.getCredit(account), + this.subscriberBillingClient.getPaymentMethod(account), + this.subscriberBillingClient.getCredit(account), ]); return { @@ -82,14 +48,6 @@ export class AccountPaymentDetailsComponent { }; }), shareReplay({ bufferSize: 1, refCount: false }), - catchError((error: unknown) => { - if (error instanceof RedirectError) { - return from(this.router.navigate(error.path, { relativeTo: error.relativeTo })).pipe( - switchMap(() => EMPTY), - ); - } - throw error; - }), ); view$: Observable = merge( @@ -99,10 +57,7 @@ export class AccountPaymentDetailsComponent { constructor( private accountService: AccountService, - private activatedRoute: ActivatedRoute, - private billingClient: SubscriberBillingClient, - private configService: ConfigService, - private router: Router, + private subscriberBillingClient: SubscriberBillingClient, ) {} setPaymentMethod = (paymentMethod: MaskedPaymentMethod) => { diff --git a/apps/web/src/app/billing/individual/premium/premium.component.html b/apps/web/src/app/billing/individual/premium/premium.component.html index 3f0f97541df..52ebe7803df 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.html +++ b/apps/web/src/app/billing/individual/premium/premium.component.html @@ -70,7 +70,7 @@ (onLicenseFileUploaded)="onLicenseFileSelectedChanged()" /> -
+

{{ "addons" | i18n }}

@@ -93,15 +93,25 @@

{{ "summary" | i18n }}

{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
- {{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB × + {{ "additionalStorageGb" | i18n }}: {{ formGroup.value.additionalStorage || 0 }} GB × {{ storageGBPrice | currency: "$" }} = {{ additionalStorageCost | currency: "$" }}

{{ "paymentInformation" | i18n }}

- - +
+ + + + +
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} diff --git a/apps/web/src/app/billing/individual/premium/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts index 974c22455ff..d5062e34881 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -9,36 +9,34 @@ import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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"; import { SyncService } from "@bitwarden/common/platform/sync"; import { ToastService } from "@bitwarden/components"; - -import { PaymentComponent } from "../../shared/payment/payment.component"; -import { TaxInfoComponent } from "../../shared/tax-info.component"; +import { TaxClient } from "@bitwarden/web-vault/app/billing/clients"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, + getBillingAddressFromForm, +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { tokenizablePaymentMethodToLegacyEnum } from "@bitwarden/web-vault/app/billing/payment/types"; @Component({ templateUrl: "./premium.component.html", standalone: false, + providers: [TaxClient], }) export class PremiumComponent { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; + @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; protected hasPremiumFromAnyOrganization$: Observable; - protected addOnFormGroup = new FormGroup({ + protected formGroup = new FormGroup({ additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]), - }); - - protected licenseFormGroup = new FormGroup({ - file: new FormControl(null, [Validators.required]), + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), }); protected cloudWebVaultURL: string; @@ -53,16 +51,14 @@ export class PremiumComponent { private activatedRoute: ActivatedRoute, private apiService: ApiService, private billingAccountProfileStateService: BillingAccountProfileStateService, - private configService: ConfigService, private environmentService: EnvironmentService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private router: Router, private syncService: SyncService, private toastService: ToastService, - private tokenService: TokenService, - private taxService: TaxServiceAbstraction, private accountService: AccountService, + private taxClient: TaxClient, ) { this.isSelfHost = this.platformUtilsService.isSelfHost(); @@ -93,11 +89,13 @@ export class PremiumComponent { ) .subscribe(); - this.addOnFormGroup.controls.additionalStorage.valueChanges - .pipe(debounceTime(1000), takeUntilDestroyed()) - .subscribe(() => { - this.refreshSalesTax(); - }); + this.formGroup.valueChanges + .pipe( + debounceTime(1000), + switchMap(async () => await this.refreshSalesTax()), + takeUntilDestroyed(), + ) + .subscribe(); } finalizeUpgrade = async () => { @@ -117,53 +115,21 @@ export class PremiumComponent { navigateToSubscriptionPage = (): Promise => this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); - onLicenseFileSelected = (event: Event): void => { - const element = event.target as HTMLInputElement; - this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null; - }; - - submitPremiumLicense = async (): Promise => { - this.licenseFormGroup.markAllAsTouched(); - - if (this.licenseFormGroup.invalid) { - return this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("selectFile"), - }); - } - - const emailVerified = await this.tokenService.getEmailVerified(); - if (!emailVerified) { - return this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("verifyEmailFirst"), - }); - } - - const formData = new FormData(); - formData.append("license", this.licenseFormGroup.value.file); - - await this.apiService.postAccountLicense(formData); - await this.finalizeUpgrade(); - await this.postFinalizeUpgrade(); - }; - submitPayment = async (): Promise => { - this.taxInfoComponent.taxFormGroup.markAllAsTouched(); - if (this.taxInfoComponent.taxFormGroup.invalid) { + if (this.formGroup.invalid) { return; } - const { type, token } = await this.paymentComponent.tokenize(); + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + + const legacyEnum = tokenizablePaymentMethodToLegacyEnum(paymentMethod.type); const formData = new FormData(); - formData.append("paymentMethodType", type.toString()); - formData.append("paymentToken", token); - formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString()); - formData.append("country", this.taxInfoComponent.country); - formData.append("postalCode", this.taxInfoComponent.postalCode); + formData.append("paymentMethodType", legacyEnum.toString()); + formData.append("paymentToken", paymentMethod.token); + formData.append("additionalStorageGb", this.formGroup.value.additionalStorage.toString()); + formData.append("country", this.formGroup.value.billingAddress.country); + formData.append("postalCode", this.formGroup.value.billingAddress.postalCode); await this.apiService.postPremium(formData); await this.finalizeUpgrade(); @@ -171,7 +137,7 @@ export class PremiumComponent { }; protected get additionalStorageCost(): number { - return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage; + return this.storageGBPrice * this.formGroup.value.additionalStorage; } protected get premiumURL(): string { @@ -190,35 +156,18 @@ export class PremiumComponent { await this.postFinalizeUpgrade(); } - private refreshSalesTax(): void { - if (!this.taxInfoComponent.country || !this.taxInfoComponent.postalCode) { + private async refreshSalesTax(): Promise { + if (this.formGroup.invalid) { return; } - const request: PreviewIndividualInvoiceRequest = { - passwordManager: { - additionalStorage: this.addOnFormGroup.value.additionalStorage, - }, - taxInformation: { - postalCode: this.taxInfoComponent.postalCode, - country: this.taxInfoComponent.country, - }, - }; - this.taxService - .previewIndividualInvoice(request) - .then((invoice) => { - this.estimatedTax = invoice.taxAmount; - }) - .catch((error) => { - this.toastService.showToast({ - title: "", - variant: "error", - message: this.i18nService.t(error.message), - }); - }); - } + const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); - protected onTaxInformationChanged(): void { - this.refreshSalesTax(); + const taxAmounts = await this.taxClient.previewTaxForPremiumSubscriptionPurchase( + this.formGroup.value.additionalStorage, + billingAddress, + ); + + this.estimatedTax = taxAmounts.tax; } } diff --git a/apps/web/src/app/billing/individual/subscription.component.html b/apps/web/src/app/billing/individual/subscription.component.html index fa2eb0412a9..f9a46cf56ad 100644 --- a/apps/web/src/app/billing/individual/subscription.component.html +++ b/apps/web/src/app/billing/individual/subscription.component.html @@ -3,10 +3,7 @@ {{ "subscription" | i18n }} - @let paymentMethodPageData = paymentDetailsPageData$ | async; - {{ - paymentMethodPageData.textKey | i18n - }} + {{ "paymentDetails" | i18n }} {{ "billingHistory" | i18n }} diff --git a/apps/web/src/app/billing/individual/subscription.component.ts b/apps/web/src/app/billing/individual/subscription.component.ts index c6a20a9f6a3..2a08ec85127 100644 --- a/apps/web/src/app/billing/individual/subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription.component.ts @@ -1,12 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { map, Observable, switchMap } from "rxjs"; +import { Observable, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @Component({ @@ -15,32 +13,16 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl }) export class SubscriptionComponent implements OnInit { hasPremium$: Observable; - paymentDetailsPageData$: Observable<{ - route: string; - textKey: string; - }>; - selfHosted: boolean; constructor( private platformUtilsService: PlatformUtilsService, billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, - private configService: ConfigService, ) { this.hasPremium$ = accountService.activeAccount$.pipe( switchMap((account) => billingAccountProfileStateService.hasPremiumPersonally$(account.id)), ); - - this.paymentDetailsPageData$ = this.configService - .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) - .pipe( - map((managePaymentDetailsOutsideCheckout) => - managePaymentDetailsOutsideCheckout - ? { route: "payment-details", textKey: "paymentDetails" } - : { route: "payment-method", textKey: "paymentMethod" }, - ), - ); } ngOnInit() { diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index f899b8eccb4..abd7bdb155a 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -328,24 +328,60 @@ *ngIf="formGroup.value.productTier !== productTypes.Free || isSubscriptionCanceled" >

{{ "paymentMethod" | i18n }}

-

- - {{ paymentSource?.description }} - - {{ "changePaymentMethod" | i18n }} - +

+ @switch (paymentMethod.type) { + @case ("bankAccount") { + + {{ paymentMethod.bankName }}, *{{ paymentMethod.last4 }} + @if (paymentMethod.hostedVerificationUrl) { + - {{ "unverified" | i18n }} + } + + {{ "changePaymentMethod" | i18n }} + + } + @case ("card") { +

+ @let cardBrandIcon = getCardBrandIcon(); + @if (cardBrandIcon !== null) { + + } @else { + + } + {{ paymentMethod.brand | titlecase }}, *{{ paymentMethod.last4 }}, + {{ paymentMethod.expiration }} + + {{ "changePaymentMethod" | i18n }} + +

+ } + @case ("payPal") { + + {{ paymentMethod.email }} + + {{ "changePaymentMethod" | i18n }} + + } + }

- - + + + +

diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 6fc2dc57ba2..2b5c27e0f09 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -12,9 +12,9 @@ import { } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { firstValueFrom, map, Subject, switchMap, takeUntil } from "rxjs"; +import { combineLatest, firstValueFrom, map, Subject, switchMap, takeUntil } from "rxjs"; +import { debounceTime } from "rxjs/operators"; -import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { @@ -28,28 +28,8 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/ import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { - BillingApiServiceAbstraction, - BillingInformation, - OrganizationBillingServiceAbstraction as OrganizationBillingService, - OrganizationInformation, - PaymentInformation, - PlanInformation, -} from "@bitwarden/common/billing/abstractions"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { - PaymentMethodType, - PlanInterval, - PlanType, - ProductTierType, -} from "@bitwarden/common/billing/enums"; -import { TaxInformation } from "@bitwarden/common/billing/models/domain"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response"; +import { PlanInterval, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -57,6 +37,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { OrganizationId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { + CardComponent, DIALOG_DATA, DialogConfig, DialogRef, @@ -64,11 +45,25 @@ import { ToastService, } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { UserId } from "@bitwarden/user-core"; +import { + OrganizationSubscriptionPlan, + SubscriberBillingClient, + TaxClient, +} from "@bitwarden/web-vault/app/billing/clients"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, + getBillingAddressFromForm, +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { + BillingAddress, + getCardBrandIcon, + MaskedPaymentMethod, +} from "@bitwarden/web-vault/app/billing/payment/types"; +import { BitwardenSubscriber } from "@bitwarden/web-vault/app/billing/types"; import { BillingNotificationService } from "../services/billing-notification.service"; import { BillingSharedModule } from "../shared/billing-shared.module"; -import { PaymentComponent } from "../shared/payment/payment.component"; type ChangePlanDialogParams = { organizationId: string; @@ -111,11 +106,16 @@ interface OnSuccessArgs { @Component({ templateUrl: "./change-plan-dialog.component.html", - imports: [BillingSharedModule], + imports: [ + BillingSharedModule, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, + CardComponent, + ], + providers: [SubscriberBillingClient, TaxClient], }) export class ChangePlanDialogComponent implements OnInit, OnDestroy { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent; + @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent: EnterPaymentMethodComponent; @Input() acceptingSponsorship = false; @Input() organizationId: string; @@ -172,7 +172,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { clientOwnerEmail: ["", [Validators.email]], plan: [this.plan], productTier: [this.productTier], - // planInterval: [1], + }); + + billingFormGroup = this.formBuilder.group({ + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), }); planType: string; @@ -183,7 +187,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { secretsManagerPlans: PlanResponse[]; organization: Organization; sub: OrganizationSubscriptionResponse; - billing: BillingResponse; dialogHeaderName: string; currentPlanName: string; showPayment: boolean = false; @@ -191,15 +194,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { currentPlan: PlanResponse; isCardStateDisabled = false; focusedIndex: number | null = null; - accountCredit: number; - paymentSource?: PaymentSourceResponse; plans: ListResponse; isSubscriptionCanceled: boolean = false; secretsManagerTotal: number; - private destroy$ = new Subject(); + paymentMethod: MaskedPaymentMethod | null; + billingAddress: BillingAddress | null; - protected taxInformation: TaxInformation; + private destroy$ = new Subject(); constructor( @Inject(DIALOG_DATA) private dialogParams: ChangePlanDialogParams, @@ -215,11 +217,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private messagingService: MessagingService, private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, - private billingApiService: BillingApiServiceAbstraction, - private taxService: TaxServiceAbstraction, private accountService: AccountService, - private organizationBillingService: OrganizationBillingService, private billingNotificationService: BillingNotificationService, + private subscriberBillingClient: SubscriberBillingClient, + private taxClient: TaxClient, ) {} async ngOnInit(): Promise { @@ -242,10 +243,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ); if (this.sub?.subscription?.status !== "canceled") { try { - const { accountCredit, paymentSource } = - await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); - this.accountCredit = accountCredit; - this.paymentSource = paymentSource; + const subscriber: BitwardenSubscriber = { type: "organization", data: this.organization }; + const [paymentMethod, billingAddress] = await Promise.all([ + this.subscriberBillingClient.getPaymentMethod(subscriber), + this.subscriberBillingClient.getBillingAddress(subscriber), + ]); + + this.paymentMethod = paymentMethod; + this.billingAddress = billingAddress; } catch (error) { this.billingNotificationService.handleError(error); } @@ -307,15 +312,24 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ? 0 : (this.sub?.customerDiscount?.percentOff ?? 0); - this.setInitialPlanSelection(); - this.loading = false; - - const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); - this.taxInformation = TaxInformation.from(taxInfo); - + await this.setInitialPlanSelection(); if (!this.isSubscriptionCanceled) { - this.refreshSalesTax(); + await this.refreshSalesTax(); } + + combineLatest([ + this.billingFormGroup.controls.billingAddress.controls.country.valueChanges, + this.billingFormGroup.controls.billingAddress.controls.postalCode.valueChanges, + this.billingFormGroup.controls.billingAddress.controls.taxId.valueChanges, + ]) + .pipe( + debounceTime(1000), + switchMap(async () => await this.refreshSalesTax()), + takeUntil(this.destroy$), + ) + .subscribe(); + + this.loading = false; } resolveHeaderName(subscription: OrganizationSubscriptionResponse): string { @@ -333,10 +347,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ); } - setInitialPlanSelection() { + async setInitialPlanSelection() { this.focusedIndex = this.selectableProducts.length - 1; if (!this.isSubscriptionCanceled) { - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + await this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); } } @@ -344,10 +358,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return this.selectableProducts.find((product) => product.productTier === productTier); } - isPaymentSourceEmpty() { - return this.paymentSource === null || this.paymentSource === undefined; - } - isSecretsManagerTrial(): boolean { return ( this.sub?.subscription?.items?.some((item) => @@ -356,13 +366,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ); } - planTypeChanged() { - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + async planTypeChanged() { + await this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); } - updateInterval(event: number) { + async updateInterval(event: number) { this.selectedInterval = event; - this.planTypeChanged(); + await this.planTypeChanged(); } protected getPlanIntervals() { @@ -460,7 +470,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } } - protected selectPlan(plan: PlanResponse) { + protected async selectPlan(plan: PlanResponse) { if ( this.selectedInterval === PlanInterval.Monthly && plan.productTier == ProductTierType.Families @@ -475,7 +485,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.formGroup.patchValue({ productTier: plan.productTier }); try { - this.refreshSalesTax(); + await this.refreshSalesTax(); } catch { this.estimatedTax = 0; } @@ -489,19 +499,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { get upgradeRequiresPaymentMethod() { const isFreeTier = this.organization?.productTierType === ProductTierType.Free; const shouldHideFree = !this.showFree; - const hasNoPaymentSource = !this.paymentSource; + const hasNoPaymentSource = !this.paymentMethod; return isFreeTier && shouldHideFree && hasNoPaymentSource; } - get selectedSecretsManagerPlan() { - let planResponse: PlanResponse; - if (this.secretsManagerPlans) { - return this.secretsManagerPlans.find((plan) => plan.type === this.selectedPlan.type); - } - return planResponse; - } - get selectedPlanInterval() { if (this.isSubscriptionCanceled) { return this.currentPlan.isAnnual ? "year" : "month"; @@ -591,8 +593,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return 0; } - const result = plan.PasswordManager.seatPrice * Math.abs(this.sub?.seats || 0); - return result; + return plan.PasswordManager.seatPrice * Math.abs(this.sub?.seats || 0); } secretsManagerSeatTotal(plan: PlanResponse, seats: number): number { @@ -746,39 +747,22 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.formGroup.controls.additionalSeats.setValue(1); } - changedCountry() { - this.paymentComponent.showBankAccount = this.taxInformation.country === "US"; - - if ( - !this.paymentComponent.showBankAccount && - this.paymentComponent.selected === PaymentMethodType.BankAccount - ) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - - protected taxInformationChanged(event: TaxInformation): void { - this.taxInformation = event; - this.changedCountry(); - this.refreshSalesTax(); - } - submit = async () => { - if (this.taxComponent !== undefined && !this.taxComponent.validate()) { - this.taxComponent.markAllAsTouched(); + this.formGroup.markAllAsTouched(); + this.billingFormGroup.markAllAsTouched(); + if (this.formGroup.invalid || (this.billingFormGroup.invalid && !this.paymentMethod)) { return; } const doSubmit = async (): Promise => { - const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - let orgId: string = null; + let orgId: string; const sub = this.sub?.subscription; const isCanceled = sub?.status === "canceled"; const isCancelledDowngradedToFreeOrg = sub?.cancelled && this.organization.productTierType === ProductTierType.Free; if (isCanceled || isCancelledDowngradedToFreeOrg) { - await this.restartSubscription(activeUserId); + await this.restartSubscription(); orgId = this.organizationId; } else { orgId = await this.updateOrganization(); @@ -795,9 +779,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { await this.syncService.fullSync(true); if (!this.acceptingSponsorship && !this.isInTrialFlow) { - // 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(["/organizations/" + orgId + "/billing/subscription"]); + await this.router.navigate(["/organizations/" + orgId + "/billing/subscription"]); } if (this.isInTrialFlow) { @@ -818,46 +800,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.dialogRef.close(); }; - private async restartSubscription(activeUserId: UserId) { - const org = await this.organizationApiService.get(this.organizationId); - const organization: OrganizationInformation = { - name: org.name, - billingEmail: org.billingEmail, - }; - - const filteredPlan = this.plans.data - .filter((plan) => plan.productTier === this.selectedPlan.productTier && !plan.legacyYear) - .find((plan) => { - const isSameBillingCycle = plan.isAnnual === this.selectedPlan.isAnnual; - return isSameBillingCycle; - }); - - const plan: PlanInformation = { - type: filteredPlan.type, - passwordManagerSeats: org.seats, - }; - - if (org.useSecretsManager) { - plan.subscribeToSecretsManager = true; - plan.secretsManagerSeats = org.smSeats; - } - - const { type, token } = await this.paymentComponent.tokenize(); - const paymentMethod: [string, PaymentMethodType] = [token, type]; - - const payment: PaymentInformation = { + private async restartSubscription() { + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + const billingAddress = getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress); + await this.subscriberBillingClient.restartSubscription( + { type: "organization", data: this.organization }, paymentMethod, - billing: this.getBillingInformationFromTaxInfoComponent(), - }; - - await this.organizationBillingService.restartSubscription( - this.organization.id, - { - organization, - plan, - payment, - }, - activeUserId, + billingAddress, ); } @@ -875,25 +824,25 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.formGroup.controls.premiumAccessAddon.value; request.planType = this.selectedPlan.type; if (this.showPayment) { - request.billingAddressCountry = this.taxInformation.country; - request.billingAddressPostalCode = this.taxInformation.postalCode; + request.billingAddressCountry = this.billingFormGroup.controls.billingAddress.value.country; + request.billingAddressPostalCode = + this.billingFormGroup.controls.billingAddress.value.postalCode; } // Secrets Manager this.buildSecretsManagerRequest(request); - if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) { - const tokenizedPaymentSource = await this.paymentComponent.tokenize(); - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, + if (this.upgradeRequiresPaymentMethod || this.showPayment || !this.paymentMethod) { + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + const billingAddress = getBillingAddressFromForm( + this.billingFormGroup.controls.billingAddress, ); - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, - ); + const subscriber: BitwardenSubscriber = { type: "organization", data: this.organization }; + await Promise.all([ + this.subscriberBillingClient.updatePaymentMethod(subscriber, paymentMethod, null), + this.subscriberBillingClient.updateBillingAddress(subscriber, billingAddress), + ]); } // Backfill pub/priv key if necessary @@ -931,18 +880,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return text; } - private getBillingInformationFromTaxInfoComponent(): BillingInformation { - return { - country: this.taxInformation.country, - postalCode: this.taxInformation.postalCode, - taxId: this.taxInformation.taxId, - addressLine1: this.taxInformation.line1, - addressLine2: this.taxInformation.line2, - city: this.taxInformation.city, - state: this.taxInformation.state, - }; - } - private buildSecretsManagerRequest(request: OrganizationUpgradeRequest): void { request.useSecretsManager = this.organization.useSecretsManager; if (!this.organization.useSecretsManager) { @@ -1002,25 +939,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } calculateTotalAppliedDiscount(total: number) { - const discountedTotal = total * (this.discountPercentageFromSub / 100); - return discountedTotal; - } - - get paymentSourceClasses() { - if (this.paymentSource == null) { - return []; - } - switch (this.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - case PaymentMethodType.Check: - return ["bwi-billing"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } + return total * (this.discountPercentageFromSub / 100); } resolvePlanName(productTier: ProductTierType) { @@ -1064,9 +983,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } } - onFocus(index: number) { + async onFocus(index: number) { this.focusedIndex = index; - this.selectPlan(this.selectableProducts[index]); + await this.selectPlan(this.selectableProducts[index]); } isCardDisabled(index: number): boolean { @@ -1078,58 +997,44 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return index; } - private refreshSalesTax(): void { - if ( - this.taxInformation === undefined || - !this.taxInformation.country || - !this.taxInformation.postalCode - ) { + private async refreshSalesTax(): Promise { + if (this.billingFormGroup.controls.billingAddress.invalid && !this.billingAddress) { return; } - const request: PreviewOrganizationInvoiceRequest = { - organizationId: this.organizationId, - passwordManager: { - additionalStorage: 0, - plan: this.selectedPlan?.type, - seats: this.sub.seats, - }, - taxInformation: { - postalCode: this.taxInformation.postalCode, - country: this.taxInformation.country, - taxId: this.taxInformation.taxId, - }, + const getPlanFromLegacyEnum = (planType: PlanType): OrganizationSubscriptionPlan => { + switch (planType) { + case PlanType.FamiliesAnnually: + return { tier: "families", cadence: "annually" }; + case PlanType.TeamsMonthly: + return { tier: "teams", cadence: "monthly" }; + case PlanType.TeamsAnnually: + return { tier: "teams", cadence: "annually" }; + case PlanType.EnterpriseMonthly: + return { tier: "enterprise", cadence: "monthly" }; + case PlanType.EnterpriseAnnually: + return { tier: "enterprise", cadence: "annually" }; + } }; - if (this.organization.useSecretsManager) { - request.secretsManager = { - seats: this.sub.smSeats, - additionalMachineAccounts: - this.sub.smServiceAccounts - this.sub.plan.SecretsManager.baseServiceAccount, - }; - } + const billingAddress = this.billingFormGroup.controls.billingAddress.valid + ? getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress) + : this.billingAddress; - this.taxService - .previewOrganizationInvoice(request) - .then((invoice) => { - this.estimatedTax = invoice.taxAmount; - }) - .catch((error) => { - const translatedMessage = this.i18nService.t(error.message); - this.toastService.showToast({ - title: "", - variant: "error", - message: - !translatedMessage || translatedMessage === "" ? error.message : translatedMessage, - }); - }); + const taxAmounts = await this.taxClient.previewTaxForOrganizationSubscriptionPlanChange( + this.organizationId, + getPlanFromLegacyEnum(this.selectedPlan.type), + billingAddress, + ); + + this.estimatedTax = taxAmounts.tax; } protected canUpdatePaymentInformation(): boolean { return ( this.upgradeRequiresPaymentMethod || this.showPayment || - this.isPaymentSourceEmpty() || + !this.paymentMethod || this.isSubscriptionCanceled ); } @@ -1146,4 +1051,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return this.i18nService.t("upgrade"); } } + + get supportsTaxId() { + return this.formGroup.value.productTier !== ProductTierType.Families; + } + + getCardBrandIcon = () => getCardBrandIcon(this.paymentMethod); } diff --git a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts index 692791db855..5c8df483587 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts @@ -11,7 +11,6 @@ import { WebPlatformUtilsService } from "../../core/web-platform-utils.service"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component"; -import { OrganizationPaymentMethodComponent } from "./payment-method/organization-payment-method.component"; const routes: Routes = [ { @@ -26,17 +25,6 @@ const routes: Routes = [ : OrganizationSubscriptionCloudComponent, data: { titleId: "subscription" }, }, - { - path: "payment-method", - component: OrganizationPaymentMethodComponent, - canActivate: [ - organizationPermissionsGuard((org) => org.canEditPaymentMethods), - organizationIsUnmanaged, - ], - data: { - titleId: "paymentMethod", - }, - }, { path: "payment-details", component: OrganizationPaymentDetailsComponent, diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index 707a854de02..90ba04c4fa4 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -17,7 +17,6 @@ import { OrganizationBillingRoutingModule } from "./organization-billing-routing import { OrganizationPlansComponent } from "./organization-plans.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component"; -import { OrganizationPaymentMethodComponent } from "./payment-method/organization-payment-method.component"; import { SecretsManagerAdjustSubscriptionComponent } from "./sm-adjust-subscription.component"; import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component"; import { SubscriptionHiddenComponent } from "./subscription-hidden.component"; @@ -45,7 +44,6 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; SecretsManagerSubscribeStandaloneComponent, SubscriptionHiddenComponent, SubscriptionStatusComponent, - OrganizationPaymentMethodComponent, ], }) export class OrganizationBillingModule {} diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index 3b765927c3c..6234fc6e6e3 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -404,17 +404,16 @@

{{ paymentDesc }}

- - - + + } + + > +
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }} diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 820bee950eb..cbeedc454dc 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -11,10 +11,9 @@ import { } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { firstValueFrom, merge, Subject, takeUntil } from "rxjs"; import { debounceTime, map, switchMap } from "rxjs/operators"; -import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { @@ -32,24 +31,12 @@ import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-conso import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { - PaymentMethodType, - PlanSponsorshipType, - PlanType, - ProductTierType, -} from "@bitwarden/common/billing/enums"; -import { TaxInformation } from "@bitwarden/common/billing/models/domain"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; +import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -59,10 +46,20 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { + OrganizationSubscriptionPlan, + SubscriberBillingClient, + TaxClient, +} from "@bitwarden/web-vault/app/billing/clients"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, + getBillingAddressFromForm, +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { tokenizablePaymentMethodToLegacyEnum } from "@bitwarden/web-vault/app/billing/payment/types"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared"; -import { PaymentComponent } from "../shared/payment/payment.component"; interface OnSuccessArgs { organizationId: string; @@ -78,11 +75,16 @@ const Allowed2020PlansForLegacyProviders = [ @Component({ selector: "app-organization-plans", templateUrl: "organization-plans.component.html", - imports: [BillingSharedModule, OrganizationCreateModule], + imports: [ + BillingSharedModule, + OrganizationCreateModule, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, + ], + providers: [SubscriberBillingClient, TaxClient], }) export class OrganizationPlansComponent implements OnInit, OnDestroy { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent; + @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; @Input() organizationId?: string; @Input() showFree = true; @@ -105,8 +107,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private _productTier = ProductTierType.Free; - protected taxInformation: TaxInformation; - @Input() get plan(): PlanType { return this._plan; @@ -135,10 +135,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder); - selfHostedForm = this.formBuilder.group({ - file: [null, [Validators.required]], - }); - formGroup = this.formBuilder.group({ name: [""], billingEmail: ["", [Validators.email]], @@ -152,6 +148,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { secretsManager: this.secretsManagerSubscription, }); + billingFormGroup = this.formBuilder.group({ + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), + }); + passwordManagerPlans: PlanResponse[]; secretsManagerPlans: PlanResponse[]; organization: Organization; @@ -179,10 +180,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private organizationApiService: OrganizationApiServiceAbstraction, private providerApiService: ProviderApiServiceAbstraction, private toastService: ToastService, - private configService: ConfigService, - private billingApiService: BillingApiServiceAbstraction, - private taxService: TaxServiceAbstraction, private accountService: AccountService, + private subscriberBillingClient: SubscriberBillingClient, + private taxClient: TaxClient, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); } @@ -199,9 +199,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { ); this.billing = await this.organizationApiService.getBilling(this.organizationId); this.sub = await this.organizationApiService.getSubscription(this.organizationId); - this.taxInformation = await this.organizationApiService.getTaxInfo(this.organizationId); - } else if (!this.selfHosted) { - this.taxInformation = await this.apiService.getTaxInfo(); + const billingAddress = await this.subscriberBillingClient.getBillingAddress({ + type: "organization", + data: this.organization, + }); + this.billingFormGroup.controls.billingAddress.patchValue({ + ...billingAddress, + taxId: billingAddress?.taxId?.value, + }); } if (!this.selfHosted) { @@ -268,15 +273,17 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.loading = false; - this.formGroup.valueChanges.pipe(debounceTime(1000), takeUntil(this.destroy$)).subscribe(() => { - this.refreshSalesTax(); - }); - - this.secretsManagerForm.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - this.refreshSalesTax(); - }); + merge( + this.formGroup.valueChanges, + this.billingFormGroup.valueChanges, + this.secretsManagerForm.valueChanges, + ) + .pipe( + debounceTime(1000), + switchMap(async () => await this.refreshSalesTax()), + takeUntil(this.destroy$), + ) + .subscribe(); if (this.enableSecretsManagerByDefault && this.selectedSecretsManagerPlan) { this.secretsManagerSubscription.patchValue({ @@ -587,34 +594,13 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.changedProduct(); } - protected changedCountry(): void { - this.paymentComponent.showBankAccount = this.taxInformation?.country === "US"; - if ( - !this.paymentComponent.showBankAccount && - this.paymentComponent.selected === PaymentMethodType.BankAccount - ) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - - protected onTaxInformationChanged(event: TaxInformation): void { - this.taxInformation = event; - this.changedCountry(); - this.refreshSalesTax(); - } - protected cancel(): void { this.onCanceled.emit(); } - protected setSelectedFile(event: Event): void { - const fileInputEl = event.target; - this.selectedFile = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null; - } - submit = async () => { - if (this.taxComponent && !this.taxComponent.validate()) { - this.taxComponent.markAllAsTouched(); + this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { return; } @@ -688,46 +674,54 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } } - private refreshSalesTax(): void { - if (!this.taxComponent.validate()) { + private async refreshSalesTax(): Promise { + if (this.billingFormGroup.controls.billingAddress.invalid) { return; } - const request: PreviewOrganizationInvoiceRequest = { - organizationId: this.organizationId, - passwordManager: { - additionalStorage: this.formGroup.controls.additionalStorage.value, - plan: this.formGroup.controls.plan.value, - sponsoredPlan: this.planSponsorshipType, - seats: this.formGroup.controls.additionalSeats.value, - }, - taxInformation: { - postalCode: this.taxInformation.postalCode, - country: this.taxInformation.country, - taxId: this.taxInformation.taxId, - }, + const getPlanFromLegacyEnum = (): OrganizationSubscriptionPlan => { + switch (this.formGroup.value.plan) { + case PlanType.FamiliesAnnually: + return { tier: "families", cadence: "annually" }; + case PlanType.TeamsMonthly: + return { tier: "teams", cadence: "monthly" }; + case PlanType.TeamsAnnually: + return { tier: "teams", cadence: "annually" }; + case PlanType.EnterpriseMonthly: + return { tier: "enterprise", cadence: "monthly" }; + case PlanType.EnterpriseAnnually: + return { tier: "enterprise", cadence: "annually" }; + } }; - if (this.secretsManagerForm.controls.enabled.value === true) { - request.secretsManager = { - seats: this.secretsManagerForm.controls.userSeats.value, - additionalMachineAccounts: this.secretsManagerForm.controls.additionalServiceAccounts.value, - }; - } + const billingAddress = getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress); - this.taxService - .previewOrganizationInvoice(request) - .then((invoice) => { - this.estimatedTax = invoice.taxAmount; - this.total = invoice.totalAmount; - }) - .catch((error) => { - this.toastService.showToast({ - title: "", - variant: "error", - message: this.i18nService.t(error.message), - }); - }); + const passwordManagerSeats = + this.formGroup.value.productTier === ProductTierType.Families + ? 1 + : this.formGroup.value.additionalSeats; + + const taxAmounts = await this.taxClient.previewTaxForOrganizationSubscriptionPurchase( + { + ...getPlanFromLegacyEnum(), + passwordManager: { + seats: passwordManagerSeats, + additionalStorage: this.formGroup.value.additionalStorage, + sponsored: false, + }, + secretsManager: this.formGroup.value.secretsManager.enabled + ? { + seats: this.secretsManagerForm.value.userSeats, + additionalServiceAccounts: this.secretsManagerForm.value.additionalServiceAccounts, + standalone: false, + } + : undefined, + }, + billingAddress, + ); + + this.estimatedTax = taxAmounts.tax; + this.total = taxAmounts.total; } private async updateOrganization() { @@ -738,21 +732,24 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.selectedPlan.PasswordManager.hasPremiumAccessOption && this.formGroup.controls.premiumAccessAddon.value; request.planType = this.selectedPlan.type; - request.billingAddressCountry = this.taxInformation?.country; - request.billingAddressPostalCode = this.taxInformation?.postalCode; + request.billingAddressCountry = this.billingFormGroup.value.billingAddress.country; + request.billingAddressPostalCode = this.billingFormGroup.value.billingAddress.postalCode; // Secrets Manager this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod) { - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = await this.paymentComponent.tokenize(); - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, - ); - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, + if (this.billingFormGroup.invalid) { + return; + } + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + await this.subscriberBillingClient.updatePaymentMethod( + { type: "organization", data: this.organization }, + paymentMethod, + { + country: this.billingFormGroup.value.billingAddress.country, + postalCode: this.billingFormGroup.value.billingAddress.postalCode, + }, ); } @@ -791,23 +788,31 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.selectedPlan.type === PlanType.Free) { request.planType = PlanType.Free; } else { - const { type, token } = await this.paymentComponent.tokenize(); + if (this.billingFormGroup.invalid) { + return; + } - request.paymentToken = token; - request.paymentMethodType = type; + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + + const billingAddress = getBillingAddressFromForm( + this.billingFormGroup.controls.billingAddress, + ); + + request.paymentToken = paymentMethod.token; + request.paymentMethodType = tokenizablePaymentMethodToLegacyEnum(paymentMethod.type); request.additionalSeats = this.formGroup.controls.additionalSeats.value; request.additionalStorageGb = this.formGroup.controls.additionalStorage.value; request.premiumAccessAddon = this.selectedPlan.PasswordManager.hasPremiumAccessOption && this.formGroup.controls.premiumAccessAddon.value; request.planType = this.selectedPlan.type; - request.billingAddressPostalCode = this.taxInformation?.postalCode; - request.billingAddressCountry = this.taxInformation?.country; - request.taxIdNumber = this.taxInformation?.taxId; - request.billingAddressLine1 = this.taxInformation?.line1; - request.billingAddressLine2 = this.taxInformation?.line2; - request.billingAddressCity = this.taxInformation?.city; - request.billingAddressState = this.taxInformation?.state; + request.billingAddressPostalCode = billingAddress.postalCode; + request.billingAddressCountry = billingAddress.country; + request.taxIdNumber = billingAddress.taxId?.value; + request.billingAddressLine1 = billingAddress.line1; + request.billingAddressLine2 = billingAddress.line2; + request.billingAddressCity = billingAddress.city; + request.billingAddressState = billingAddress.state; } // Secrets Manager diff --git a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts index 47742ba0a88..b2bf27e726a 100644 --- a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts +++ b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts @@ -1,15 +1,11 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute } from "@angular/router"; import { BehaviorSubject, - catchError, combineLatest, - EMPTY, filter, firstValueFrom, - from, lastValueFrom, - map, merge, Observable, of, @@ -22,15 +18,13 @@ import { withLatestFrom, } from "rxjs"; -import { - getOrganizationById, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { getById } from "@bitwarden/common/platform/misc"; import { DialogService } from "@bitwarden/components"; import { CommandDefinition, MessageListener } from "@bitwarden/messaging"; import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; @@ -54,13 +48,6 @@ import { TaxIdWarningType } from "@bitwarden/web-vault/app/billing/warnings/type import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -class RedirectError { - constructor( - public path: string[], - public relativeTo: ActivatedRoute, - ) {} -} - type View = { organization: BitwardenSubscriber; paymentMethod: MaskedPaymentMethod | null; @@ -93,24 +80,12 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy { switchMap((userId) => this.organizationService .organizations$(userId) - .pipe(getOrganizationById(this.activatedRoute.snapshot.params.organizationId)), + .pipe(getById(this.activatedRoute.snapshot.params.organizationId)), ), filter((organization): organization is Organization => !!organization), ); private load$: Observable = this.organization$.pipe( - switchMap((organization) => - this.configService - .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) - .pipe( - map((managePaymentDetailsOutsideCheckout) => { - if (!managePaymentDetailsOutsideCheckout) { - throw new RedirectError(["../payment-method"], this.activatedRoute); - } - return organization; - }), - ), - ), mapOrganizationToSubscriber, switchMap(async (organization) => { const getTaxIdWarning = firstValueFrom( @@ -132,14 +107,6 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy { taxIdWarning, }; }), - catchError((error: unknown) => { - if (error instanceof RedirectError) { - return from(this.router.navigate(error.path, { relativeTo: error.relativeTo })).pipe( - switchMap(() => EMPTY), - ); - } - throw error; - }), ); view$: Observable = merge( @@ -159,7 +126,6 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy { private messageListener: MessageListener, private organizationService: OrganizationService, private organizationWarningsService: OrganizationWarningsService, - private router: Router, private subscriberBillingClient: SubscriberBillingClient, ) {} diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html deleted file mode 100644 index ab31147e916..00000000000 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - {{ "loading" | i18n }} - - - - -

- {{ accountCreditHeaderText }} -

-

{{ Math.abs(accountCredit) | currency: "$" }}

-

{{ "creditAppliedDesc" | i18n }}

- -
- - -

{{ "paymentMethod" | i18n }}

-

{{ "noPaymentMethod" | i18n }}

- - - -

- - {{ paymentSource.description }} - - {{ "unverified" | i18n }} -

-
- -

- {{ "paymentChargedWithUnpaidSubscription" | i18n }} -

-
-
-
diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts deleted file mode 100644 index 4106ee4f9cd..00000000000 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { Location } from "@angular/common"; -import { Component, OnDestroy } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, firstValueFrom, from, lastValueFrom, map, switchMap } from "rxjs"; - -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { - OrganizationService, - getOrganizationById, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { TaxInformation } from "@bitwarden/common/billing/models/domain"; -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; -import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import { BillingNotificationService } from "../../services/billing-notification.service"; -import { - AddCreditDialogResult, - openAddCreditDialog, -} from "../../shared/add-credit-dialog.component"; -import { - AdjustPaymentDialogComponent, - AdjustPaymentDialogResultType, -} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component"; -import { - TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE, - TrialPaymentDialogComponent, -} from "../../shared/trial-payment-dialog/trial-payment-dialog.component"; - -@Component({ - templateUrl: "./organization-payment-method.component.html", - standalone: false, -}) -export class OrganizationPaymentMethodComponent implements OnDestroy { - organizationId!: string; - isUnpaid = false; - accountCredit?: number; - paymentSource?: PaymentSourceResponse; - subscriptionStatus?: string; - organization?: Organization; - organizationSubscriptionResponse?: OrganizationSubscriptionResponse; - - loading = true; - - protected readonly Math = Math; - launchPaymentModalAutomatically = false; - - protected taxInformation?: TaxInformation; - - constructor( - private activatedRoute: ActivatedRoute, - private billingApiService: BillingApiServiceAbstraction, - protected organizationApiService: OrganizationApiServiceAbstraction, - private dialogService: DialogService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private router: Router, - private toastService: ToastService, - private location: Location, - private organizationService: OrganizationService, - private accountService: AccountService, - protected syncService: SyncService, - private billingNotificationService: BillingNotificationService, - private configService: ConfigService, - ) { - combineLatest([ - this.activatedRoute.params, - this.configService.getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout), - ]) - .pipe( - switchMap(([{ organizationId }, managePaymentDetailsOutsideCheckout]) => { - if (this.platformUtilsService.isSelfHost()) { - return from(this.router.navigate(["/settings/subscription"])); - } - - if (managePaymentDetailsOutsideCheckout) { - return from( - this.router.navigate(["../payment-details"], { relativeTo: this.activatedRoute }), - ); - } - - this.organizationId = organizationId; - return from(this.load()); - }), - takeUntilDestroyed(), - ) - .subscribe(); - - const state = this.router.getCurrentNavigation()?.extras?.state; - // In case the above state is undefined or null, we use redundantState - const redundantState: any = location.getState(); - const queryParam = this.activatedRoute.snapshot.queryParamMap.get( - "launchPaymentModalAutomatically", - ); - if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { - this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; - } else if ( - redundantState && - Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") - ) { - this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; - } else { - this.launchPaymentModalAutomatically = queryParam === "true"; - } - } - ngOnDestroy(): void { - this.launchPaymentModalAutomatically = false; - } - - protected addAccountCredit = async (): Promise => { - if (this.subscriptionStatus === "trialing") { - const hasValidBillingAddress = await this.checkBillingAddressForTrialingOrg(); - if (!hasValidBillingAddress) { - return; - } - } - const dialogRef = openAddCreditDialog(this.dialogService, { - data: { - organizationId: this.organizationId, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AddCreditDialogResult.Added) { - await this.load(); - } - }; - - protected load = async (): Promise => { - this.loading = true; - try { - const { accountCredit, paymentSource, subscriptionStatus, taxInformation } = - await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); - this.accountCredit = accountCredit; - this.paymentSource = paymentSource; - this.subscriptionStatus = subscriptionStatus; - this.taxInformation = taxInformation; - this.isUnpaid = this.subscriptionStatus === "unpaid"; - - if (this.organizationId) { - const organizationSubscriptionPromise = this.organizationApiService.getSubscription( - this.organizationId, - ); - - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - if (!userId) { - throw new Error("User ID is not found"); - } - - const organizationPromise = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId)), - ); - - [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ - organizationSubscriptionPromise, - organizationPromise, - ]); - - if (!this.organization) { - throw new Error("Organization is not found"); - } - if (!this.paymentSource) { - throw new Error("Payment source is not found"); - } - } - // If the flag `launchPaymentModalAutomatically` is set to true, - // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. - // This delay ensures that any prior UI/rendering operations complete before triggering the modal. - if (this.launchPaymentModalAutomatically) { - window.setTimeout(async () => { - await this.changePayment(); - this.launchPaymentModalAutomatically = false; - this.location.replaceState(this.location.path(), "", {}); - }, 800); - } - } catch (error) { - this.billingNotificationService.handleError(error); - } finally { - this.loading = false; - } - }; - - protected updatePaymentMethod = async (): Promise => { - const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { - data: { - initialPaymentMethod: this.paymentSource?.type, - organizationId: this.organizationId, - productTier: this.organization?.productTierType, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustPaymentDialogResultType.Submitted) { - await this.load(); - } - }; - - changePayment = async () => { - const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, { - data: { - organizationId: this.organizationId, - subscription: this.organizationSubscriptionResponse!, - productTierType: this.organization!.productTierType, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) { - this.location.replaceState(this.location.path(), "", {}); - if (this.launchPaymentModalAutomatically && !this.organization?.enabled) { - await this.syncService.fullSync(true); - } - this.launchPaymentModalAutomatically = false; - await this.load(); - } - }; - - protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise => { - await this.billingApiService.verifyOrganizationBankAccount(this.organizationId, request); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("verifiedBankAccount"), - }); - }; - - protected get accountCreditHeaderText(): string { - const hasAccountCredit = this.accountCredit && this.accountCredit > 0; - const key = hasAccountCredit ? "accountCredit" : "accountBalance"; - return this.i18nService.t(key); - } - - protected get paymentSourceClasses() { - if (this.paymentSource == null) { - return []; - } - switch (this.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - case PaymentMethodType.Check: - return ["bwi-billing"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } - } - - protected get subscriptionIsUnpaid(): boolean { - return this.subscriptionStatus === "unpaid"; - } - - protected get updatePaymentSourceButtonText(): string { - const key = this.paymentSource == null ? "addPaymentMethod" : "changePaymentMethod"; - return this.i18nService.t(key); - } - - private async checkBillingAddressForTrialingOrg(): Promise { - const hasBillingAddress = this.taxInformation != null; - if (!hasBillingAddress) { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("billingAddressRequiredToAddCredit"), - }); - return false; - } - return true; - } -} diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts index c7a297cc28b..53f72558089 100644 --- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts @@ -15,8 +15,6 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogRef, DialogService } from "@bitwarden/components"; import { OrganizationBillingClient } from "@bitwarden/web-vault/app/billing/clients"; @@ -35,7 +33,6 @@ import { TaxIdWarningTypes } from "@bitwarden/web-vault/app/billing/warnings/typ describe("OrganizationWarningsService", () => { let service: OrganizationWarningsService; - let configService: MockProxy; let dialogService: MockProxy; let i18nService: MockProxy; let organizationApiService: MockProxy; @@ -57,7 +54,6 @@ describe("OrganizationWarningsService", () => { }); beforeEach(() => { - configService = mock(); dialogService = mock(); i18nService = mock(); organizationApiService = mock(); @@ -94,7 +90,6 @@ describe("OrganizationWarningsService", () => { TestBed.configureTestingModule({ providers: [ OrganizationWarningsService, - { provide: ConfigService, useValue: configService }, { provide: DialogService, useValue: dialogService }, { provide: I18nService, useValue: i18nService }, { provide: OrganizationApiServiceAbstraction, useValue: organizationApiService }, @@ -466,7 +461,6 @@ describe("OrganizationWarningsService", () => { } as OrganizationWarningsResponse); dialogService.openSimpleDialog.mockResolvedValue(true); - configService.getFeatureFlag.mockResolvedValue(false); router.navigate.mockResolvedValue(true); service.showInactiveSubscriptionDialog$(organization).subscribe({ @@ -478,11 +472,8 @@ describe("OrganizationWarningsService", () => { acceptButtonText: "Continue", cancelButtonText: "Close", }); - expect(configService.getFeatureFlag).toHaveBeenCalledWith( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); expect(router.navigate).toHaveBeenCalledWith( - ["organizations", "org-id-123", "billing", "payment-method"], + ["organizations", "org-id-123", "billing", "payment-details"], { state: { launchPaymentModalAutomatically: true } }, ); done(); @@ -497,7 +488,6 @@ describe("OrganizationWarningsService", () => { } as OrganizationWarningsResponse); dialogService.openSimpleDialog.mockResolvedValue(true); - configService.getFeatureFlag.mockResolvedValue(true); router.navigate.mockResolvedValue(true); service.showInactiveSubscriptionDialog$(organization).subscribe({ @@ -522,7 +512,6 @@ describe("OrganizationWarningsService", () => { service.showInactiveSubscriptionDialog$(organization).subscribe({ complete: () => { expect(dialogService.openSimpleDialog).toHaveBeenCalled(); - expect(configService.getFeatureFlag).not.toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled(); done(); }, diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts index c6bb1bc231b..46a34def28b 100644 --- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts @@ -16,8 +16,6 @@ import { take } from "rxjs/operators"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; @@ -53,7 +51,6 @@ export class OrganizationWarningsService { taxIdWarningRefreshed$ = this.taxIdWarningRefreshedSubject.asObservable(); constructor( - private configService: ConfigService, private dialogService: DialogService, private i18nService: I18nService, private organizationApiService: OrganizationApiServiceAbstraction, @@ -196,14 +193,8 @@ export class OrganizationWarningsService { cancelButtonText: this.i18nService.t("close"), }); if (confirmed) { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - const route = managePaymentDetailsOutsideCheckout - ? "payment-details" - : "payment-method"; await this.router.navigate( - ["organizations", `${organization.id}`, "billing", route], + ["organizations", `${organization.id}`, "billing", "payment-details"], { state: { launchPaymentModalAutomatically: true }, }, diff --git a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts index c33d805aed7..5f5e3442935 100644 --- a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts +++ b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts @@ -5,7 +5,7 @@ import { DialogService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; import { BitwardenSubscriber } from "../../types"; -import { MaskedPaymentMethod } from "../types"; +import { getCardBrandIcon, MaskedPaymentMethod } from "../types"; import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dialog.component"; @@ -40,9 +40,9 @@ import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dial } @case ("card") {

- @let brandIcon = getBrandIconForCard(); - @if (brandIcon !== null) { - + @let cardBrandIcon = getCardBrandIcon(); + @if (cardBrandIcon !== null) { + } @else { } @@ -74,16 +74,6 @@ export class DisplayPaymentMethodComponent { @Input({ required: true }) paymentMethod!: MaskedPaymentMethod | null; @Output() updated = new EventEmitter(); - protected availableCardIcons: Record = { - amex: "card-amex", - diners: "card-diners-club", - discover: "card-discover", - jcb: "card-jcb", - mastercard: "card-mastercard", - unionpay: "card-unionpay", - visa: "card-visa", - }; - constructor(private dialogService: DialogService) {} changePaymentMethod = async (): Promise => { @@ -100,13 +90,5 @@ export class DisplayPaymentMethodComponent { } }; - protected getBrandIconForCard = (): string | null => { - if (this.paymentMethod?.type !== "card") { - return null; - } - - return this.paymentMethod.brand in this.availableCardIcons - ? this.availableCardIcons[this.paymentMethod.brand] - : null; - }; + protected getCardBrandIcon = () => getCardBrandIcon(this.paymentMethod); } diff --git a/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts b/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts index de2f2f94497..6e356097d32 100644 --- a/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts @@ -11,10 +11,7 @@ import { ToastService, } from "@bitwarden/components"; import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; -import { - BillingAddress, - getTaxIdTypeForCountry, -} from "@bitwarden/web-vault/app/billing/payment/types"; +import { BillingAddress } from "@bitwarden/web-vault/app/billing/payment/types"; import { BitwardenSubscriber } from "@bitwarden/web-vault/app/billing/types"; import { TaxIdWarningType, @@ -22,7 +19,10 @@ import { } from "@bitwarden/web-vault/app/billing/warnings/types"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -import { EnterBillingAddressComponent } from "./enter-billing-address.component"; +import { + EnterBillingAddressComponent, + getBillingAddressFromForm, +} from "./enter-billing-address.component"; type DialogParams = { subscriber: BitwardenSubscriber; @@ -104,13 +104,7 @@ export class EditBillingAddressDialogComponent { return; } - const { taxId, ...addressFields } = this.formGroup.getRawValue(); - - const taxIdType = taxId ? getTaxIdTypeForCountry(addressFields.country) : null; - - const billingAddress = taxIdType - ? { ...addressFields, taxId: { code: taxIdType.code, value: taxId! } } - : { ...addressFields, taxId: null }; + const billingAddress = getBillingAddressFromForm(this.formGroup); const result = await this.billingClient.updateBillingAddress( this.dialogParams.subscriber, diff --git a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts index 7659b7ed5ca..3f68c12c897 100644 --- a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts @@ -24,6 +24,17 @@ export interface BillingAddressControls { export type BillingAddressFormGroup = FormGroup>; +export const getBillingAddressFromForm = (formGroup: BillingAddressFormGroup): BillingAddress => + getBillingAddressFromControls(formGroup.getRawValue()); + +export const getBillingAddressFromControls = (controls: BillingAddressControls) => { + const { taxId, ...addressFields } = controls; + const taxIdType = taxId ? getTaxIdTypeForCountry(addressFields.country) : null; + return taxIdType + ? { ...addressFields, taxId: { code: taxIdType.code, value: taxId! } } + : { ...addressFields, taxId: null }; +}; + type Scenario = | { type: "checkout"; @@ -67,54 +78,56 @@ type Scenario = />

-
- - {{ "address1" | i18n }} - - -
-
- - {{ "address2" | i18n }} - - -
-
- - {{ "cityTown" | i18n }} - - -
-
- - {{ "stateProvince" | i18n }} - - -
+ @if (scenario.type === "update") { +
+ + {{ "address1" | i18n }} + + +
+
+ + {{ "address2" | i18n }} + + +
+
+ + {{ "cityTown" | i18n }} + + +
+
+ + {{ "stateProvince" | i18n }} + + +
+ } @if (supportsTaxId$ | async) {
@@ -175,7 +188,7 @@ export class EnterBillingAddressComponent implements OnInit, OnDestroy { this.supportsTaxId$ = this.group.controls.country.valueChanges.pipe( startWith(this.group.value.country ?? this.selectableCountries[0].value), map((country) => { - if (!this.scenario.supportsTaxId) { + if (!this.scenario.supportsTaxId || country === "US") { return false; } diff --git a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts index 93c45b873fe..4af5226e7ee 100644 --- a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts @@ -8,7 +8,6 @@ import { PopoverModule, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; -import { PaymentLabelComponent } from "../../shared/payment/payment-label.component"; import { isTokenizablePaymentMethod, selectableCountries, @@ -16,6 +15,8 @@ import { TokenizedPaymentMethod, } from "../types"; +import { PaymentLabelComponent } from "./payment-label.component"; + type PaymentMethodOption = TokenizablePaymentMethod | "accountCredit"; type PaymentMethodFormGroup = FormGroup<{ @@ -102,7 +103,7 @@ type PaymentMethodFormGroup = FormGroup<{ - - - - -
- - - - - - - - - - - - - - - -
diff --git a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts deleted file mode 100644 index cdf72168acf..00000000000 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ /dev/null @@ -1,191 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { - getOrganizationById, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; - -export interface AddCreditDialogData { - organizationId: string; -} - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum AddCreditDialogResult { - Added = "added", - Cancelled = "cancelled", -} - -export type PayPalConfig = { - businessId?: string; - buttonAction?: string; -}; - -@Component({ - templateUrl: "add-credit-dialog.component.html", - standalone: false, -}) -export class AddCreditDialogComponent implements OnInit { - @ViewChild("ppButtonForm", { read: ElementRef, static: true }) ppButtonFormRef: ElementRef; - - paymentMethodType = PaymentMethodType; - ppButtonFormAction: string; - ppButtonBusinessId: string; - ppButtonCustomField: string; - ppLoading = false; - subject: string; - returnUrl: string; - organizationId: string; - - private userId: string; - private name: string; - private email: string; - private region: string; - - protected DialogResult = AddCreditDialogResult; - protected formGroup = new FormGroup({ - method: new FormControl(PaymentMethodType.PayPal), - creditAmount: new FormControl(null, [Validators.required]), - }); - - constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AddCreditDialogData, - private accountService: AccountService, - private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, - private organizationService: OrganizationService, - private logService: LogService, - private configService: ConfigService, - ) { - this.organizationId = data.organizationId; - const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig; - this.ppButtonFormAction = payPalConfig.buttonAction; - this.ppButtonBusinessId = payPalConfig.businessId; - } - - async ngOnInit() { - if (this.organizationId != null) { - if (this.creditAmount == null) { - this.creditAmount = "0.00"; - } - this.ppButtonCustomField = "organization_id:" + this.organizationId; - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const org = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId)), - ); - if (org != null) { - this.subject = org.name; - this.name = org.name; - } - } else { - if (this.creditAmount == null) { - this.creditAmount = "0.00"; - } - const [userId, email] = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), - ); - this.userId = userId; - this.subject = email; - this.email = this.subject; - this.ppButtonCustomField = "user_id:" + this.userId; - } - this.region = await firstValueFrom(this.configService.cloudRegion$); - this.ppButtonCustomField += ",account_credit:1"; - this.ppButtonCustomField += `,region:${this.region}`; - this.returnUrl = window.location.href; - } - - get creditAmount() { - return this.formGroup.value.creditAmount; - } - set creditAmount(value: string) { - this.formGroup.get("creditAmount").setValue(value); - } - - get method() { - return this.formGroup.value.method; - } - - submit = async () => { - if (this.creditAmount == null || this.creditAmount === "") { - return; - } - - if (this.method === PaymentMethodType.PayPal) { - this.ppButtonFormRef.nativeElement.submit(); - this.ppLoading = true; - return; - } - if (this.method === PaymentMethodType.BitPay) { - const req = new BitPayInvoiceRequest(); - req.email = this.email; - req.name = this.name; - req.credit = true; - req.amount = this.creditAmountNumber; - req.organizationId = this.organizationId; - req.userId = this.userId; - req.returnUrl = this.returnUrl; - const bitPayUrl: string = await this.apiService.postBitPayInvoice(req); - this.platformUtilsService.launchUri(bitPayUrl); - return; - } - this.dialogRef.close(AddCreditDialogResult.Added); - }; - - formatAmount() { - try { - if (this.creditAmount != null && this.creditAmount !== "") { - const floatAmount = Math.abs(parseFloat(this.creditAmount)); - if (floatAmount > 0) { - this.creditAmount = parseFloat((Math.round(floatAmount * 100) / 100).toString()) - .toFixed(2) - .toString(); - return; - } - } - } catch (e) { - this.logService.error(e); - } - this.creditAmount = ""; - } - - get creditAmountNumber(): number { - if (this.creditAmount != null && this.creditAmount !== "") { - try { - return parseFloat(this.creditAmount); - } catch (e) { - this.logService.error(e); - } - } - return null; - } -} - -/** - * Strongly typed helper to open a AddCreditDialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ -export function openAddCreditDialog( - dialogService: DialogService, - config: DialogConfig, -) { - return dialogService.open(AddCreditDialogComponent, config); -} diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html deleted file mode 100644 index 9c70908af8e..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts deleted file mode 100644 index 9944085488f..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ /dev/null @@ -1,225 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, forwardRef, Inject, OnInit, ViewChild } from "@angular/core"; - -import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PaymentMethodType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { TaxInformation } from "@bitwarden/common/billing/models/domain"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { - DIALOG_DATA, - DialogConfig, - DialogRef, - DialogService, - ToastService, -} from "@bitwarden/components"; - -import { PaymentComponent } from "../payment/payment.component"; - -export interface AdjustPaymentDialogParams { - initialPaymentMethod?: PaymentMethodType | null; - organizationId?: string; - productTier?: ProductTierType; - providerId?: string; -} - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum AdjustPaymentDialogResultType { - Closed = "closed", - Submitted = "submitted", -} - -@Component({ - templateUrl: "./adjust-payment-dialog.component.html", - standalone: false, -}) -export class AdjustPaymentDialogComponent implements OnInit { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(forwardRef(() => ManageTaxInformationComponent)) - taxInfoComponent: ManageTaxInformationComponent; - - protected readonly PaymentMethodType = PaymentMethodType; - protected readonly ResultType = AdjustPaymentDialogResultType; - - protected dialogHeader: string; - protected initialPaymentMethod: PaymentMethodType; - protected organizationId?: string; - protected productTier?: ProductTierType; - protected providerId?: string; - - protected loading = true; - - protected taxInformation: TaxInformation; - - constructor( - private apiService: ApiService, - private billingApiService: BillingApiServiceAbstraction, - private organizationApiService: OrganizationApiServiceAbstraction, - @Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogParams, - private dialogRef: DialogRef, - private i18nService: I18nService, - private toastService: ToastService, - ) { - const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod"; - this.dialogHeader = this.i18nService.t(key); - this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; - this.organizationId = this.dialogParams.organizationId; - this.productTier = this.dialogParams.productTier; - this.providerId = this.dialogParams.providerId; - } - - ngOnInit(): void { - if (this.organizationId) { - this.organizationApiService - .getTaxInfo(this.organizationId) - .then((response: TaxInfoResponse) => { - this.taxInformation = TaxInformation.from(response); - this.toggleBankAccount(); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }) - .finally(() => { - this.loading = false; - }); - } else if (this.providerId) { - this.billingApiService - .getProviderTaxInformation(this.providerId) - .then((response) => { - this.taxInformation = TaxInformation.from(response); - this.toggleBankAccount(); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }) - .finally(() => { - this.loading = false; - }); - } else { - this.apiService - .getTaxInfo() - .then((response: TaxInfoResponse) => { - this.taxInformation = TaxInformation.from(response); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }) - .finally(() => { - this.loading = false; - }); - } - } - - taxInformationChanged(event: TaxInformation) { - this.taxInformation = event; - this.toggleBankAccount(); - } - - toggleBankAccount = () => { - if (this.taxInformation.country === "US") { - this.paymentComponent.showBankAccount = !!this.organizationId || !!this.providerId; - } else { - this.paymentComponent.showBankAccount = false; - if (this.paymentComponent.selected === PaymentMethodType.BankAccount) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - }; - - submit = async (): Promise => { - if (!this.taxInfoComponent.validate()) { - this.taxInfoComponent.markAllAsTouched(); - return; - } - - try { - if (this.organizationId) { - await this.updateOrganizationPaymentMethod(); - } else if (this.providerId) { - await this.updateProviderPaymentMethod(); - } else { - await this.updatePremiumUserPaymentMethod(); - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("updatedPaymentMethod"), - }); - - this.dialogRef.close(AdjustPaymentDialogResultType.Submitted); - } catch (error) { - const msg = typeof error == "object" ? error.message : error; - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t(msg) || msg, - }); - } - }; - - private updateOrganizationPaymentMethod = async () => { - const paymentSource = await this.paymentComponent.tokenize(); - - const request = new UpdatePaymentMethodRequest(); - request.paymentSource = paymentSource; - request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); - - await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request); - }; - - private updatePremiumUserPaymentMethod = async () => { - const { type, token } = await this.paymentComponent.tokenize(); - - const request = new PaymentRequest(); - request.paymentMethodType = type; - request.paymentToken = token; - request.country = this.taxInformation.country; - request.postalCode = this.taxInformation.postalCode; - request.taxId = this.taxInformation.taxId; - request.state = this.taxInformation.state; - request.line1 = this.taxInformation.line1; - request.line2 = this.taxInformation.line2; - request.city = this.taxInformation.city; - request.state = this.taxInformation.state; - await this.apiService.postAccountPayment(request); - }; - - private updateProviderPaymentMethod = async () => { - const paymentSource = await this.paymentComponent.tokenize(); - - const request = new UpdatePaymentMethodRequest(); - request.paymentSource = paymentSource; - request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); - - await this.billingApiService.updateProviderPaymentMethod(this.providerId, request); - }; - - protected get showTaxIdField(): boolean { - if (this.organizationId) { - switch (this.productTier) { - case ProductTierType.Free: - case ProductTierType.Families: - return false; - default: - return true; - } - } else { - return !!this.providerId; - } - } - - static open = ( - dialogService: DialogService, - dialogConfig: DialogConfig, - ) => - dialogService.open(AdjustPaymentDialogComponent, dialogConfig); -} diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 7322f047551..fb593b39328 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -1,46 +1,40 @@ import { NgModule } from "@angular/core"; import { BannerModule } from "@bitwarden/components"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, +} from "@bitwarden/web-vault/app/billing/payment/components"; import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; -import { AddCreditDialogComponent } from "./add-credit-dialog.component"; -import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component"; import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; -import { PaymentComponent } from "./payment/payment.component"; -import { PaymentMethodComponent } from "./payment-method.component"; import { PlanCardComponent } from "./plan-card/plan-card.component"; import { PricingSummaryComponent } from "./pricing-summary/pricing-summary.component"; import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component"; import { OrganizationSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/organization-self-hosting-license-uploader.component"; import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component"; -import { TaxInfoComponent } from "./tax-info.component"; import { TrialPaymentDialogComponent } from "./trial-payment-dialog/trial-payment-dialog.component"; import { UpdateLicenseDialogComponent } from "./update-license-dialog.component"; import { UpdateLicenseComponent } from "./update-license.component"; -import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-account.component"; @NgModule({ imports: [ SharedModule, - TaxInfoComponent, HeaderModule, BannerModule, - PaymentComponent, - VerifyBankAccountComponent, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, ], declarations: [ - AddCreditDialogComponent, BillingHistoryComponent, - PaymentMethodComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, - AdjustPaymentDialogComponent, AdjustStorageDialogComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, @@ -50,14 +44,11 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac ], exports: [ SharedModule, - TaxInfoComponent, BillingHistoryComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, - VerifyBankAccountComponent, - PaymentComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, ], diff --git a/apps/web/src/app/billing/shared/index.ts b/apps/web/src/app/billing/shared/index.ts index 54ab5bc0a2a..466d1d3e586 100644 --- a/apps/web/src/app/billing/shared/index.ts +++ b/apps/web/src/app/billing/shared/index.ts @@ -1,4 +1,2 @@ export * from "./billing-shared.module"; -export * from "./payment-method.component"; export * from "./sm-subscribe.component"; -export * from "./tax-info.component"; diff --git a/apps/web/src/app/billing/shared/payment-method.component.html b/apps/web/src/app/billing/shared/payment-method.component.html deleted file mode 100644 index 81ed7e5a631..00000000000 --- a/apps/web/src/app/billing/shared/payment-method.component.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - -

{{ "paymentMethod" | i18n }}

- - - - {{ "loading" | i18n }} - - - -

- {{ (isCreditBalance ? "accountCredit" : "accountBalance") | i18n }} -

-

{{ creditOrBalance | currency: "$" }}

-

{{ "creditAppliedDesc" | i18n }}

- -
- -

{{ "paymentMethod" | i18n }}

-

{{ "noPaymentMethod" | i18n }}

- - -

- {{ "verifyBankAccountDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }} -

-
- - {{ "amountX" | i18n: "1" }} - - $0. - - - {{ "amountX" | i18n: "2" }} - - $0. - - -
-
-

- - {{ paymentSource.description }} -

-
- -

- {{ "paymentChargedWithUnpaidSubscription" | i18n }} -

-
-
-
diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts deleted file mode 100644 index b6431843b83..00000000000 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { Location } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { FormBuilder, FormControl, Validators } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, lastValueFrom, map } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { - OrganizationService, - getOrganizationById, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { BillingPaymentResponse } from "@bitwarden/common/billing/models/response/billing-payment.response"; -import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; -import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank.request"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; -import { - AdjustPaymentDialogComponent, - AdjustPaymentDialogResultType, -} from "./adjust-payment-dialog/adjust-payment-dialog.component"; - -@Component({ - templateUrl: "payment-method.component.html", - standalone: false, -}) -export class PaymentMethodComponent implements OnInit, OnDestroy { - loading = false; - firstLoaded = false; - billing?: BillingPaymentResponse; - org?: OrganizationSubscriptionResponse; - sub?: SubscriptionResponse; - paymentMethodType = PaymentMethodType; - organizationId?: string; - isUnpaid = false; - organization?: Organization; - - verifyBankForm = this.formBuilder.group({ - amount1: new FormControl(0, [ - Validators.required, - Validators.max(99), - Validators.min(0), - ]), - amount2: new FormControl(0, [ - Validators.required, - Validators.max(99), - Validators.min(0), - ]), - }); - - launchPaymentModalAutomatically = false; - constructor( - protected apiService: ApiService, - protected organizationApiService: OrganizationApiServiceAbstraction, - protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, - private router: Router, - private location: Location, - private route: ActivatedRoute, - private formBuilder: FormBuilder, - private dialogService: DialogService, - private toastService: ToastService, - private organizationService: OrganizationService, - private accountService: AccountService, - protected syncService: SyncService, - private configService: ConfigService, - ) { - const state = this.router.getCurrentNavigation()?.extras?.state; - // In case the above state is undefined or null, we use redundantState - const redundantState: any = location.getState(); - if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { - this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; - } else if ( - redundantState && - Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") - ) { - this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; - } else { - this.launchPaymentModalAutomatically = false; - } - } - - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.params.subscribe(async (params) => { - if (params.organizationId) { - this.organizationId = params.organizationId; - } else if (this.platformUtilsService.isSelfHost()) { - // 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(["/settings/subscription"]); - return; - } - - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - - if (managePaymentDetailsOutsideCheckout) { - await this.router.navigate(["../payment-details"], { relativeTo: this.route }); - } - - await this.load(); - this.firstLoaded = true; - }); - } - - load = async () => { - if (this.loading) { - return; - } - this.loading = true; - if (this.forOrganization) { - const billingPromise = this.organizationApiService.getBilling(this.organizationId!); - const organizationSubscriptionPromise = this.organizationApiService.getSubscription( - this.organizationId!, - ); - - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - if (!userId) { - throw new Error("User ID is not found"); - } - - const organizationPromise = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId!)), - ); - - [this.billing, this.org, this.organization] = await Promise.all([ - billingPromise, - organizationSubscriptionPromise, - organizationPromise, - ]); - } else { - const billingPromise = this.apiService.getUserBillingPayment(); - const subPromise = this.apiService.getUserSubscription(); - - [this.billing, this.sub] = await Promise.all([billingPromise, subPromise]); - } - // TODO: Eslint upgrade. Please resolve this since the ?? does nothing - // eslint-disable-next-line no-constant-binary-expression - this.isUnpaid = this.subscription?.status === "unpaid" ?? false; - this.loading = false; - // If the flag `launchPaymentModalAutomatically` is set to true, - // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. - // This delay ensures that any prior UI/rendering operations complete before triggering the modal. - if (this.launchPaymentModalAutomatically) { - window.setTimeout(async () => { - await this.changePayment(); - this.launchPaymentModalAutomatically = false; - this.location.replaceState(this.location.path(), "", {}); - }, 800); - } - }; - - addCredit = async () => { - if (this.forOrganization) { - const dialogRef = openAddCreditDialog(this.dialogService, { - data: { - organizationId: this.organizationId!, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AddCreditDialogResult.Added) { - await this.load(); - } - } - }; - - changePayment = async () => { - const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { - data: { - organizationId: this.organizationId, - initialPaymentMethod: this.paymentSource !== null ? this.paymentSource.type : null, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustPaymentDialogResultType.Submitted) { - this.location.replaceState(this.location.path(), "", {}); - if (this.launchPaymentModalAutomatically && !this.organization?.enabled) { - await this.syncService.fullSync(true); - } - this.launchPaymentModalAutomatically = false; - await this.load(); - } - }; - - verifyBank = async () => { - if (this.loading || !this.forOrganization) { - return; - } - - const request = new VerifyBankRequest(); - request.amount1 = this.verifyBankForm.value.amount1!; - request.amount2 = this.verifyBankForm.value.amount2!; - await this.organizationApiService.verifyBank(this.organizationId!, request); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("verifiedBankAccount"), - }); - await this.load(); - }; - - get isCreditBalance() { - return this.billing == null || this.billing.balance <= 0; - } - - get creditOrBalance() { - return Math.abs(this.billing != null ? this.billing.balance : 0); - } - - get paymentSource() { - return this.billing != null ? this.billing.paymentSource : null; - } - - get forOrganization() { - return this.organizationId != null; - } - - get paymentSourceClasses() { - if (this.paymentSource == null) { - return []; - } - switch (this.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - case PaymentMethodType.Check: - return ["bwi-billing"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } - } - - get subscription() { - return this.sub?.subscription ?? this.org?.subscription ?? null; - } - - ngOnDestroy(): void { - this.launchPaymentModalAutomatically = false; - } -} diff --git a/apps/web/src/app/billing/shared/payment/payment-label.component.html b/apps/web/src/app/billing/shared/payment/payment-label.component.html deleted file mode 100644 index a931b0524e3..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment-label.component.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - -
- - - ({{ "required" | i18n }}) - -
diff --git a/apps/web/src/app/billing/shared/payment/payment.component.html b/apps/web/src/app/billing/shared/payment/payment.component.html deleted file mode 100644 index d1356c20854..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment.component.html +++ /dev/null @@ -1,149 +0,0 @@ -
-
- - - - - {{ "creditCard" | i18n }} - - - - - - {{ "bankAccount" | i18n }} - - - - - - {{ "payPal" | i18n }} - - - - - - {{ "accountCredit" | i18n }} - - - -
- - -
-
- - {{ "number" | i18n }} - -
-
-
- Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay -
-
- - {{ "expiration" | i18n }} - -
-
-
- - {{ "securityCodeSlashCVV" | i18n }} - - - - -
-
-
-
- - - - {{ "requiredToVerifyBankAccountWithStripe" | i18n }} - -
- - {{ "routingNumber" | i18n }} - - - - {{ "accountNumber" | i18n }} - - - - {{ "accountHolderName" | i18n }} - - - - {{ "bankAccountType" | i18n }} - - - - - - -
-
- - -
-
- {{ "paypalClickSubmit" | i18n }} -
-
- - - - {{ "makeSureEnoughCredit" | i18n }} - - - -
diff --git a/apps/web/src/app/billing/shared/payment/payment.component.ts b/apps/web/src/app/billing/shared/payment/payment.component.ts deleted file mode 100644 index 08476e9952f..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment.component.ts +++ /dev/null @@ -1,215 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Subject } from "rxjs"; -import { takeUntil } from "rxjs/operators"; - -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { SharedModule } from "../../../shared"; -import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; - -import { PaymentLabelComponent } from "./payment-label.component"; - -/** - * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, - * optionally, submit it using the {@link onSubmit} function if it is provided. - */ -@Component({ - selector: "app-payment", - templateUrl: "./payment.component.html", - imports: [BillingServicesModule, SharedModule, PaymentLabelComponent], -}) -export class PaymentComponent implements OnInit, OnDestroy { - /** Show account credit as a payment option. */ - @Input() showAccountCredit: boolean = true; - /** Show bank account as a payment option. */ - @Input() showBankAccount: boolean = true; - /** Show PayPal as a payment option. */ - @Input() showPayPal: boolean = true; - - /** The payment method selected by default when the component renders. */ - @Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card; - /** If provided, will be invoked with the tokenized payment source during form submission. */ - @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; - - @Input() private bankAccountWarningOverride?: string; - - @Output() submitted = new EventEmitter(); - - private destroy$ = new Subject(); - - protected formGroup = new FormGroup({ - paymentMethod: new FormControl(null), - bankInformation: new FormGroup({ - routingNumber: new FormControl("", [Validators.required]), - accountNumber: new FormControl("", [Validators.required]), - accountHolderName: new FormControl("", [Validators.required]), - accountHolderType: new FormControl("", [Validators.required]), - }), - }); - - protected PaymentMethodType = PaymentMethodType; - - constructor( - private billingApiService: BillingApiServiceAbstraction, - private braintreeService: BraintreeService, - private i18nService: I18nService, - private stripeService: StripeService, - ) {} - - ngOnInit(): void { - this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod); - - this.stripeService.loadStripe( - { - cardNumber: "#stripe-card-number", - cardExpiry: "#stripe-card-expiry", - cardCvc: "#stripe-card-cvc", - }, - this.initialPaymentMethod === PaymentMethodType.Card, - ); - - if (this.showPayPal) { - this.braintreeService.loadBraintree( - "#braintree-container", - this.initialPaymentMethod === PaymentMethodType.PayPal, - ); - } - - this.formGroup - .get("paymentMethod") - .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((type) => { - this.onPaymentMethodChange(type); - }); - } - - /** Programmatically select the provided payment method. */ - select = (paymentMethod: PaymentMethodType) => { - this.formGroup.get("paymentMethod").patchValue(paymentMethod); - }; - - protected submit = async () => { - const { type, token } = await this.tokenize(); - await this.onSubmit?.({ type, token }); - this.submitted.emit(type); - }; - - validate = () => { - if (!this.usingBankAccount) { - return true; - } - - this.formGroup.controls.bankInformation.markAllAsTouched(); - return this.formGroup.controls.bankInformation.valid; - }; - - /** - * Tokenize the payment method information entered by the user against one of our payment providers. - * - * - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} - * - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} - * - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} - * */ - async tokenize(): Promise<{ type: PaymentMethodType; token: string }> { - const type = this.selected; - - if (this.usingStripe) { - const clientSecret = await this.billingApiService.createSetupIntent(type); - - if (this.usingBankAccount) { - this.formGroup.markAllAsTouched(); - if (this.formGroup.valid) { - const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { - accountHolderName: this.formGroup.value.bankInformation.accountHolderName, - routingNumber: this.formGroup.value.bankInformation.routingNumber, - accountNumber: this.formGroup.value.bankInformation.accountNumber, - accountHolderType: this.formGroup.value.bankInformation.accountHolderType, - }); - return { - type, - token, - }; - } else { - throw "Invalid input provided. Please ensure all required fields are filled out correctly and try again."; - } - } - - if (this.usingCard) { - const token = await this.stripeService.setupCardPaymentMethod(clientSecret); - return { - type, - token, - }; - } - } - - if (this.usingPayPal) { - const token = await this.braintreeService.requestPaymentMethod(); - return { - type, - token, - }; - } - - if (this.usingAccountCredit) { - return { - type: PaymentMethodType.Credit, - token: null, - }; - } - - return null; - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - this.stripeService.unloadStripe(); - if (this.showPayPal) { - this.braintreeService.unloadBraintree(); - } - } - - private onPaymentMethodChange(type: PaymentMethodType): void { - switch (type) { - case PaymentMethodType.Card: { - this.stripeService.mountElements(); - break; - } - case PaymentMethodType.PayPal: { - this.braintreeService.createDropin(); - break; - } - } - } - - get selected(): PaymentMethodType { - return this.formGroup.value.paymentMethod; - } - - protected get usingAccountCredit(): boolean { - return this.selected === PaymentMethodType.Credit; - } - - protected get usingBankAccount(): boolean { - return this.selected === PaymentMethodType.BankAccount; - } - - protected get usingCard(): boolean { - return this.selected === PaymentMethodType.Card; - } - - protected get usingPayPal(): boolean { - return this.selected === PaymentMethodType.PayPal; - } - - private get usingStripe(): boolean { - return this.usingBankAccount || this.usingCard; - } -} diff --git a/apps/web/src/app/billing/shared/tax-info.component.html b/apps/web/src/app/billing/shared/tax-info.component.html deleted file mode 100644 index ca2ae046f6e..00000000000 --- a/apps/web/src/app/billing/shared/tax-info.component.html +++ /dev/null @@ -1,83 +0,0 @@ -
-
-
- - {{ "country" | i18n }} - - - - -
-
- - {{ "zipPostalCode" | i18n }} - - -
-
- - {{ "address1" | i18n }} - - -
-
- - {{ "address2" | i18n }} - - -
-
- - {{ "cityTown" | i18n }} - - -
-
- - {{ "stateProvince" | i18n }} - - -
-
- - {{ "taxIdNumber" | i18n }} - - -
-
-
diff --git a/apps/web/src/app/billing/shared/tax-info.component.ts b/apps/web/src/app/billing/shared/tax-info.component.ts deleted file mode 100644 index 35c4a3fcc4e..00000000000 --- a/apps/web/src/app/billing/shared/tax-info.component.ts +++ /dev/null @@ -1,199 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; -import { debounceTime } from "rxjs/operators"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { CountryListItem } from "@bitwarden/common/billing/models/domain"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; - -import { SharedModule } from "../../shared"; - -/** - * @deprecated Use `ManageTaxInformationComponent` instead. - */ -@Component({ - selector: "app-tax-info", - templateUrl: "tax-info.component.html", - imports: [SharedModule], -}) -export class TaxInfoComponent implements OnInit, OnDestroy { - private destroy$ = new Subject(); - - @Input() trialFlow = false; - @Output() countryChanged = new EventEmitter(); - @Output() taxInformationChanged: EventEmitter = new EventEmitter(); - - taxFormGroup = new FormGroup({ - country: new FormControl(null, [Validators.required]), - postalCode: new FormControl(null, [Validators.required]), - taxId: new FormControl(null), - line1: new FormControl(null), - line2: new FormControl(null), - city: new FormControl(null), - state: new FormControl(null), - }); - - protected isTaxSupported: boolean; - - loading = true; - organizationId: string; - providerId: string; - countryList: CountryListItem[] = this.taxService.getCountries(); - - constructor( - private apiService: ApiService, - private route: ActivatedRoute, - private logService: LogService, - private organizationApiService: OrganizationApiServiceAbstraction, - private taxService: TaxServiceAbstraction, - ) {} - - get country(): string { - return this.taxFormGroup.controls.country.value; - } - - get postalCode(): string { - return this.taxFormGroup.controls.postalCode.value; - } - - get taxId(): string { - return this.taxFormGroup.controls.taxId.value; - } - - get line1(): string { - return this.taxFormGroup.controls.line1.value; - } - - get line2(): string { - return this.taxFormGroup.controls.line2.value; - } - - get city(): string { - return this.taxFormGroup.controls.city.value; - } - - get state(): string { - return this.taxFormGroup.controls.state.value; - } - - get showTaxIdField(): boolean { - return !!this.organizationId; - } - - async ngOnInit() { - // Provider setup - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.route.queryParams.subscribe((params) => { - this.providerId = params.providerId; - }); - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent?.parent?.params.subscribe(async (params) => { - this.organizationId = params.organizationId; - if (this.organizationId) { - try { - const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); - if (taxInfo) { - this.taxFormGroup.controls.taxId.setValue(taxInfo.taxId); - this.taxFormGroup.controls.state.setValue(taxInfo.state); - this.taxFormGroup.controls.line1.setValue(taxInfo.line1); - this.taxFormGroup.controls.line2.setValue(taxInfo.line2); - this.taxFormGroup.controls.city.setValue(taxInfo.city); - this.taxFormGroup.controls.postalCode.setValue(taxInfo.postalCode); - this.taxFormGroup.controls.country.setValue(taxInfo.country); - } - } catch (e) { - this.logService.error(e); - } - } else { - try { - const taxInfo = await this.apiService.getTaxInfo(); - if (taxInfo) { - this.taxFormGroup.controls.postalCode.setValue(taxInfo.postalCode); - this.taxFormGroup.controls.country.setValue(taxInfo.country); - } - } catch (e) { - this.logService.error(e); - } - } - - this.isTaxSupported = await this.taxService.isCountrySupported( - this.taxFormGroup.controls.country.value, - ); - - this.countryChanged.emit(); - }); - - this.taxFormGroup.controls.country.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe((value) => { - this.taxService - .isCountrySupported(this.taxFormGroup.controls.country.value) - .then((isSupported) => { - this.isTaxSupported = isSupported; - }) - .catch(() => { - this.isTaxSupported = false; - }) - .finally(() => { - if (!this.isTaxSupported) { - this.taxFormGroup.controls.taxId.setValue(null); - this.taxFormGroup.controls.line1.setValue(null); - this.taxFormGroup.controls.line2.setValue(null); - this.taxFormGroup.controls.city.setValue(null); - this.taxFormGroup.controls.state.setValue(null); - } - - this.countryChanged.emit(); - }); - this.taxInformationChanged.emit(); - }); - - this.taxFormGroup.controls.postalCode.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - this.taxInformationChanged.emit(); - }); - - this.taxFormGroup.controls.taxId.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - this.taxInformationChanged.emit(); - }); - - this.loading = false; - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - submitTaxInfo(): Promise { - this.taxFormGroup.updateValueAndValidity(); - this.taxFormGroup.markAllAsTouched(); - - const request = new ExpandedTaxInfoUpdateRequest(); - request.country = this.country; - request.postalCode = this.postalCode; - request.taxId = this.taxId; - request.line1 = this.line1; - request.line2 = this.line2; - request.city = this.city; - request.state = this.state; - - return this.organizationId - ? this.organizationApiService.updateTaxInfo( - this.organizationId, - request as ExpandedTaxInfoUpdateRequest, - ) - : this.apiService.putTaxInfo(request); - } -} diff --git a/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html index dbd2899c9e0..1b416eae1bc 100644 --- a/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html +++ b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html @@ -86,17 +86,13 @@

{{ "paymentMethod" | i18n }}

- - + + + + (); protected initialPaymentMethod: PaymentMethodType; - protected taxInformation!: TaxInformation; protected readonly ResultType = TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE; pricingSummaryData!: PricingSummaryData; + formGroup = new FormGroup({ + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), + }); + + private destroy$ = new Subject(); + constructor( @Inject(DIALOG_DATA) private dialogParams: TrialPaymentDialogParams, private dialogRef: DialogRef, @@ -93,8 +110,9 @@ export class TrialPaymentDialogComponent implements OnInit { private pricingSummaryService: PricingSummaryService, private apiService: ApiService, private toastService: ToastService, - private billingApiService: BillingApiServiceAbstraction, private organizationBillingApiServiceAbstraction: OrganizationBillingApiServiceAbstraction, + private subscriberBillingClient: SubscriberBillingClient, + private taxClient: TaxClient, ) { this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; } @@ -134,19 +152,48 @@ export class TrialPaymentDialogComponent implements OnInit { : PlanInterval.Monthly; } - const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); - this.taxInformation = TaxInformation.from(taxInfo); + const billingAddress = await this.subscriberBillingClient.getBillingAddress({ + type: "organization", + data: this.organization, + }); - this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( - this.currentPlan, - this.sub, - this.organization, - this.selectedInterval, - this.taxInformation, - this.isSecretsManagerTrial(), - ); + if (billingAddress) { + const { taxId, ...location } = billingAddress; + + this.formGroup.controls.billingAddress.patchValue({ + ...location, + taxId: taxId ? taxId.value : null, + }); + } + + await this.refreshPricingSummary(); this.plans = await this.apiService.getPlans(); + + combineLatest([ + this.formGroup.controls.billingAddress.controls.country.valueChanges.pipe( + startWith(this.formGroup.controls.billingAddress.controls.country.value), + ), + this.formGroup.controls.billingAddress.controls.postalCode.valueChanges.pipe( + startWith(this.formGroup.controls.billingAddress.controls.postalCode.value), + ), + this.formGroup.controls.billingAddress.controls.taxId.valueChanges.pipe( + startWith(this.formGroup.controls.billingAddress.controls.taxId.value), + ), + ]) + .pipe( + debounceTime(500), + switchMap(() => { + return this.refreshPricingSummary(); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } static open = ( @@ -175,14 +222,7 @@ export class TrialPaymentDialogComponent implements OnInit { await this.selectPlan(); - this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( - this.currentPlan, - this.sub, - this.organization, - this.selectedInterval, - this.taxInformation, - this.isSecretsManagerTrial(), - ); + await this.refreshPricingSummary(); } protected async selectPlan() { @@ -202,7 +242,7 @@ export class TrialPaymentDialogComponent implements OnInit { this.currentPlan = filteredPlans[0]; } try { - await this.refreshSalesTax(); + await this.refreshPricingSummary(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const translatedMessage = this.i18nService.t(errorMessage); @@ -214,72 +254,57 @@ export class TrialPaymentDialogComponent implements OnInit { } } - protected get showTaxIdField(): boolean { - switch (this.currentPlan.productTier) { - case ProductTierType.Free: - case ProductTierType.Families: - return false; - default: - return true; - } - } - - private async refreshSalesTax(): Promise { - if ( - this.taxInformation === undefined || - !this.taxInformation.country || - !this.taxInformation.postalCode - ) { - return; - } - - const request: PreviewOrganizationInvoiceRequest = { - organizationId: this.organizationId, - passwordManager: { - additionalStorage: 0, - plan: this.currentPlan?.type, - seats: this.sub.seats, - }, - taxInformation: { - postalCode: this.taxInformation.postalCode, - country: this.taxInformation.country, - taxId: this.taxInformation.taxId, - }, - }; - - if (this.organization.useSecretsManager) { - request.secretsManager = { - seats: this.sub.smSeats ?? 0, - additionalMachineAccounts: - (this.sub.smServiceAccounts ?? 0) - - (this.sub.plan.SecretsManager?.baseServiceAccount ?? 0), - }; - } - + private refreshPricingSummary = async () => { + const estimatedTax = await this.getEstimatedTax(); this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( this.currentPlan, this.sub, this.organization, this.selectedInterval, - this.taxInformation, this.isSecretsManagerTrial(), + estimatedTax, ); - } + }; - async taxInformationChanged(event: TaxInformation) { - this.taxInformation = event; - this.toggleBankAccount(); - await this.refreshSalesTax(); - } + private getEstimatedTax = async () => { + if (this.formGroup.controls.billingAddress.invalid) { + return 0; + } - toggleBankAccount = () => { - this.paymentComponent.showBankAccount = this.taxInformation.country === "US"; + const cadence = + this.currentPlan.productTier !== ProductTierType.Families + ? this.currentPlan.isAnnual + ? "annually" + : "monthly" + : null; - if ( - !this.paymentComponent.showBankAccount && - this.paymentComponent.selected === PaymentMethodType.BankAccount - ) { - this.paymentComponent.select(PaymentMethodType.Card); + const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); + + const getTierFromLegacyEnum = (organization: Organization) => { + switch (organization.productTierType) { + case ProductTierType.Families: + return "families"; + case ProductTierType.Teams: + return "teams"; + case ProductTierType.Enterprise: + return "enterprise"; + } + }; + + const tier = getTierFromLegacyEnum(this.organization); + + if (tier && cadence) { + const costs = await this.taxClient.previewTaxForOrganizationSubscriptionPlanChange( + this.organization.id, + { + tier, + cadence, + }, + billingAddress, + ); + return costs.tax; + } else { + return 0; } }; @@ -292,15 +317,24 @@ export class TrialPaymentDialogComponent implements OnInit { } async onSubscribe(): Promise { - if (!this.taxComponent.validate()) { - this.taxComponent.markAllAsTouched(); + this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { + return; } + try { - await this.updateOrganizationPaymentMethod( - this.organizationId, - this.paymentComponent, - this.taxInformation, - ); + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + if (!paymentMethod) { + return; + } + + const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); + + const subscriber: BitwardenSubscriber = { type: "organization", data: this.organization }; + await Promise.all([ + this.subscriberBillingClient.updatePaymentMethod(subscriber, paymentMethod, null), + this.subscriberBillingClient.updateBillingAddress(subscriber, billingAddress), + ]); if (this.currentPlan.type !== this.sub.planType) { const changePlanRequest = new ChangePlanFrequencyRequest(); @@ -332,20 +366,6 @@ export class TrialPaymentDialogComponent implements OnInit { } } - private async updateOrganizationPaymentMethod( - organizationId: string, - paymentComponent: PaymentComponent, - taxInformation: TaxInformation, - ): Promise { - const paymentSource = await paymentComponent.tokenize(); - - const request = new UpdatePaymentMethodRequest(); - request.paymentSource = paymentSource; - request.taxInformation = ExpandedTaxInfoUpdateRequest.From(taxInformation); - - await this.billingApiService.updateOrganizationPaymentMethod(organizationId, request); - } - resolvePlanName(productTier: ProductTierType): string { switch (productTier) { case ProductTierType.Enterprise: @@ -362,4 +382,11 @@ export class TrialPaymentDialogComponent implements OnInit { return this.i18nService.t("planNameFree"); } } + + get supportsTaxId() { + if (!this.organization) { + return false; + } + return this.organization.productTierType !== ProductTierType.Families; + } } diff --git a/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.html b/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.html deleted file mode 100644 index 1367e6e3082..00000000000 --- a/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.html +++ /dev/null @@ -1,12 +0,0 @@ - -

{{ "verifyBankAccountWithStatementDescriptorInstructions" | i18n }}

-
- - {{ "descriptorCode" | i18n }} - - - -
-
diff --git a/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.ts b/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.ts deleted file mode 100644 index b7cdfbe60a2..00000000000 --- a/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { FormBuilder, FormControl, Validators } from "@angular/forms"; - -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; - -import { SharedModule } from "../../../shared"; - -@Component({ - selector: "app-verify-bank-account", - templateUrl: "./verify-bank-account.component.html", - imports: [SharedModule], -}) -export class VerifyBankAccountComponent { - @Input() onSubmit?: (request: VerifyBankAccountRequest) => Promise; - @Output() submitted = new EventEmitter(); - - protected formGroup = this.formBuilder.group({ - descriptorCode: new FormControl(null, [ - Validators.required, - Validators.minLength(6), - Validators.maxLength(6), - ]), - }); - - constructor(private formBuilder: FormBuilder) {} - - submit = async () => { - const request = new VerifyBankAccountRequest(this.formGroup.value.descriptorCode); - await this.onSubmit?.(request); - this.submitted.emit(); - }; -} diff --git a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html index c1a33a4c8df..7377fc45484 100644 --- a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html @@ -54,17 +54,7 @@ > diff --git a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts index 0b1ddda0c12..baccabdc763 100644 --- a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts @@ -30,13 +30,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; import { UserId } from "@bitwarden/user-core"; +import { Trial } from "@bitwarden/web-vault/app/billing/trial-initiation/trial-billing-step/trial-billing-step.service"; -import { - OrganizationCreatedEvent, - SubscriptionProduct, - TrialOrganizationType, -} from "../../../billing/accounts/trial-initiation/trial-billing-step.component"; import { RouterService } from "../../../core/router.service"; +import { OrganizationCreatedEvent } from "../trial-billing-step/trial-billing-step.component"; import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component"; export type InitiationPath = @@ -95,7 +92,6 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { }); private destroy$ = new Subject(); - protected readonly SubscriptionProduct = SubscriptionProduct; protected readonly ProductType = ProductType; protected trialPaymentOptional$ = this.configService.getFeatureFlag$( FeatureFlag.TrialPaymentOptional, @@ -338,14 +334,6 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { } } - get trialOrganizationType(): TrialOrganizationType | null { - if (this.productTier === ProductTierType.Free) { - return null; - } - - return this.productTier; - } - readonly showBillingStep$ = this.trialPaymentOptional$.pipe( map((trialPaymentOptional) => { return ( @@ -434,4 +422,26 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { return null; }); } + + get trial(): Trial { + const product = + this.product === ProductType.PasswordManager ? "passwordManager" : "secretsManager"; + + const tier = + this.productTier === ProductTierType.Families + ? "families" + : this.productTier === ProductTierType.Teams + ? "teams" + : "enterprise"; + + return { + organization: { + name: this.orgInfoFormGroup.value.name!, + email: this.orgInfoFormGroup.value.billingEmail!, + }, + product, + tier, + length: this.trialLength, + }; + } } diff --git a/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.html b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.html new file mode 100644 index 00000000000..51b7f0c7117 --- /dev/null +++ b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.html @@ -0,0 +1,87 @@ +@if (!(prices$ | async)) { + +} @else { + @let prices = prices$ | async; +
+
+ +
+

{{ "billingPlanLabel" | i18n }}

+ +
+ + + {{ "annual" | i18n }} - + {{ prices.annually | currency: "$" }} + /{{ "yr" | i18n }} + + +
+ @if (prices.monthly) { +
+ + + {{ "monthly" | i18n }} - + {{ prices.monthly | currency: "$" }} + /{{ "monthAbbr" | i18n }} + + +
+ } +
+
+ +
+

{{ "paymentType" | i18n }}

+ + + + @if (trial().length === 0) { + @let label = + trial().product === "passwordManager" + ? "passwordManagerPlanPrice" + : "secretsManagerPlanPrice"; +
+ @let selectionTaxAmounts = selectionCosts$ | async; +
+ {{ label | i18n }}: {{ selectionPrice$ | async | currency: "USD $" }} +
+ {{ "estimatedTax" | i18n }}: + {{ selectionTaxAmounts.tax | currency: "USD $" }} +
+
+
+

+ {{ "total" | i18n }}: + @let interval = formGroup.value.cadence === "annually" ? "year" : "month"; + {{ selectionTaxAmounts.total | currency: "USD $" }}/{{ interval | i18n }} +

+
+ } +
+ +
+ + +
+
+
+} + + + + {{ "loading" | i18n }} + diff --git a/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.ts b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.ts new file mode 100644 index 00000000000..0f185564c2e --- /dev/null +++ b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.ts @@ -0,0 +1,160 @@ +import { Component, input, OnDestroy, OnInit, output, ViewChild } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; +import { + combineLatest, + debounceTime, + filter, + map, + Observable, + shareReplay, + startWith, + switchMap, + Subject, + firstValueFrom, +} from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ToastService } from "@bitwarden/components"; +import { TaxClient } from "@bitwarden/web-vault/app/billing/clients"; +import { + BillingAddressControls, + EnterBillingAddressComponent, + EnterPaymentMethodComponent, +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { + Cadence, + Cadences, + Prices, + Trial, + TrialBillingStepService, +} from "@bitwarden/web-vault/app/billing/trial-initiation/trial-billing-step/trial-billing-step.service"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +export interface OrganizationCreatedEvent { + organizationId: string; + planDescription: string; +} + +@Component({ + selector: "app-trial-billing-step", + templateUrl: "./trial-billing-step.component.html", + imports: [EnterPaymentMethodComponent, EnterBillingAddressComponent, SharedModule], + providers: [TaxClient, TrialBillingStepService], +}) +export class TrialBillingStepComponent implements OnInit, OnDestroy { + @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; + + protected trial = input.required(); + protected steppedBack = output(); + protected organizationCreated = output(); + + private destroy$ = new Subject(); + + protected prices$!: Observable; + + protected selectionPrice$!: Observable; + protected selectionCosts$!: Observable<{ + tax: number; + total: number; + }>; + protected selectionDescription$!: Observable; + + protected formGroup = new FormGroup({ + cadence: new FormControl(Cadences.Annually, { + nonNullable: true, + }), + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), + }); + + constructor( + private i18nService: I18nService, + private toastService: ToastService, + private trialBillingStepService: TrialBillingStepService, + ) {} + + async ngOnInit() { + const { product, tier } = this.trial(); + this.prices$ = this.trialBillingStepService.getPrices$(product, tier); + + const cadenceChanged = this.formGroup.controls.cadence.valueChanges.pipe( + startWith(Cadences.Annually), + ); + + this.selectionPrice$ = combineLatest([this.prices$, cadenceChanged]).pipe( + map(([prices, cadence]) => prices[cadence]), + filter((price): price is number => !!price), + ); + + this.selectionCosts$ = combineLatest([ + cadenceChanged, + this.formGroup.controls.billingAddress.valueChanges.pipe( + startWith(this.formGroup.controls.billingAddress.value), + filter( + (billingAddress): billingAddress is BillingAddressControls => + !!billingAddress.country && !!billingAddress.postalCode, + ), + ), + ]).pipe( + debounceTime(500), + switchMap(([cadence, billingAddress]) => + this.trialBillingStepService.getCosts(product, tier, cadence, billingAddress), + ), + startWith({ + tax: 0, + total: 0, + }), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + this.selectionDescription$ = combineLatest([this.selectionPrice$, cadenceChanged]).pipe( + map(([price, cadence]) => { + switch (cadence) { + case Cadences.Annually: + return `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`; + case Cadences.Monthly: + return `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`; + } + }), + ); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + submit = async (): Promise => { + this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { + return; + } + + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + if (!paymentMethod) { + return; + } + + const billingAddress = this.formGroup.controls.billingAddress.getRawValue(); + + const organization = await this.trialBillingStepService.startTrial( + this.trial(), + this.formGroup.value.cadence!, + billingAddress, + paymentMethod, + ); + + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("organizationCreated"), + message: this.i18nService.t("organizationReadyToGo"), + }); + + this.organizationCreated.emit({ + organizationId: organization.id, + planDescription: await firstValueFrom(this.selectionDescription$), + }); + }; + + protected stepBack = () => this.steppedBack.emit(); +} diff --git a/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.service.ts b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.service.ts new file mode 100644 index 00000000000..9e4f45ede92 --- /dev/null +++ b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.service.ts @@ -0,0 +1,209 @@ +import { Injectable } from "@angular/core"; +import { firstValueFrom, from, map, shareReplay } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { + OrganizationBillingServiceAbstraction, + SubscriptionInformation, +} from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums"; +import { TaxClient } from "@bitwarden/web-vault/app/billing/clients"; +import { + BillingAddressControls, + getBillingAddressFromControls, +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { + tokenizablePaymentMethodToLegacyEnum, + TokenizedPaymentMethod, +} from "@bitwarden/web-vault/app/billing/payment/types"; + +export const Tiers = { + Families: "families", + Teams: "teams", + Enterprise: "enterprise", +} as const; + +export const Cadences = { + Annually: "annually", + Monthly: "monthly", +} as const; + +export const Products = { + PasswordManager: "passwordManager", + SecretsManager: "secretsManager", +} as const; + +export type Tier = (typeof Tiers)[keyof typeof Tiers]; +export type Cadence = (typeof Cadences)[keyof typeof Cadences]; +export type Product = (typeof Products)[keyof typeof Products]; + +export type Prices = { + [Cadences.Annually]: number; + [Cadences.Monthly]?: number; +}; + +export interface Trial { + organization: { + name: string; + email: string; + }; + product: Product; + tier: Tier; + length: number; +} + +@Injectable() +export class TrialBillingStepService { + constructor( + private accountService: AccountService, + private apiService: ApiService, + private organizationBillingService: OrganizationBillingServiceAbstraction, + private taxClient: TaxClient, + ) {} + + private plans$ = from(this.apiService.getPlans()).pipe( + shareReplay({ bufferSize: 1, refCount: true }), + ); + + getPrices$ = (product: Product, tier: Tier) => + this.plans$.pipe( + map((plans) => { + switch (tier) { + case "families": { + const annually = plans.data.find((plan) => plan.type === PlanType.FamiliesAnnually); + return { + annually: annually!.PasswordManager.basePrice, + }; + } + case "teams": + case "enterprise": { + const annually = plans.data.find( + (plan) => + plan.type === + (tier === "teams" ? PlanType.TeamsAnnually : PlanType.EnterpriseAnnually), + ); + const monthly = plans.data.find( + (plan) => + plan.type === + (tier === "teams" ? PlanType.TeamsMonthly : PlanType.EnterpriseMonthly), + ); + switch (product) { + case "passwordManager": { + return { + annually: annually!.PasswordManager.seatPrice, + monthly: monthly!.PasswordManager.seatPrice, + }; + } + case "secretsManager": { + return { + annually: annually!.SecretsManager.seatPrice, + monthly: monthly!.SecretsManager.seatPrice, + }; + } + } + } + } + }), + ); + + getCosts = async ( + product: Product, + tier: Tier, + cadence: Cadence, + billingAddressControls: BillingAddressControls, + ): Promise<{ + tax: number; + total: number; + }> => { + const billingAddress = getBillingAddressFromControls(billingAddressControls); + return await this.taxClient.previewTaxForOrganizationSubscriptionPurchase( + { + tier, + cadence, + passwordManager: { + seats: 1, + additionalStorage: 0, + sponsored: false, + }, + secretsManager: + product === "secretsManager" + ? { + seats: 1, + additionalServiceAccounts: 0, + standalone: true, + } + : undefined, + }, + billingAddress, + ); + }; + + startTrial = async ( + trial: Trial, + cadence: Cadence, + billingAddress: BillingAddressControls, + paymentMethod: TokenizedPaymentMethod, + ): Promise => { + const getPlanType = async (tier: Tier, cadence: Cadence) => { + const plans = await firstValueFrom(this.plans$); + switch (tier) { + case "families": + return plans.data.find((plan) => plan.type === PlanType.FamiliesAnnually)!.type; + case "teams": + return plans.data.find( + (plan) => + plan.type === + (cadence === "annually" ? PlanType.TeamsAnnually : PlanType.TeamsMonthly), + )!.type; + case "enterprise": + return plans.data.find( + (plan) => + plan.type === + (cadence === "annually" ? PlanType.EnterpriseAnnually : PlanType.EnterpriseMonthly), + )!.type; + } + }; + + const legacyPaymentMethod: [string, PaymentMethodType] = [ + paymentMethod.token, + tokenizablePaymentMethodToLegacyEnum(paymentMethod.type), + ]; + const planType = await getPlanType(trial.tier, cadence); + + const request: SubscriptionInformation = { + organization: { + name: trial.organization.name, + billingEmail: trial.organization.email, + initiationPath: + trial.product === "passwordManager" + ? "Password Manager trial from marketing website" + : "Secrets Manager trial from marketing website", + }, + plan: + trial.product === "passwordManager" + ? { type: planType, passwordManagerSeats: 1 } + : { + type: planType, + passwordManagerSeats: 1, + subscribeToSecretsManager: true, + isFromSecretsManagerTrial: true, + secretsManagerSeats: 1, + }, + payment: { + paymentMethod: legacyPaymentMethod, + billing: { + country: billingAddress.country, + postalCode: billingAddress.postalCode, + taxId: billingAddress.taxId ?? undefined, + }, + skipTrial: trial.length === 0, + }, + }; + + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + return await this.organizationBillingService.purchaseSubscription(request, activeUserId); + }; +} diff --git a/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts b/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts index 06e1cce7f23..cd393f0dd5e 100644 --- a/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts +++ b/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts @@ -6,11 +6,11 @@ import { InputPasswordComponent } from "@bitwarden/auth/angular"; import { FormFieldModule } from "@bitwarden/components"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; -import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component"; import { SharedModule } from "../../shared"; import { CompleteTrialInitiationComponent } from "./complete-trial-initiation/complete-trial-initiation.component"; import { ConfirmationDetailsComponent } from "./confirmation-details.component"; +import { TrialBillingStepComponent } from "./trial-billing-step/trial-billing-step.component"; import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module"; @NgModule({ diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts index 011e7a3bce6..cd32eaf2858 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts @@ -5,8 +5,6 @@ import { filter, firstValueFrom, map, Observable, switchMap } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; import { BannerModule } from "@bitwarden/components"; @@ -41,7 +39,6 @@ export class VaultBannersComponent implements OnInit { private router: Router, private accountService: AccountService, private messageListener: MessageListener, - private configService: ConfigService, ) { this.premiumBannerVisible$ = this.activeUserId$.pipe( filter((userId): userId is UserId => userId != null), @@ -75,16 +72,12 @@ export class VaultBannersComponent implements OnInit { } async navigateToPaymentMethod(organizationId: string): Promise { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; const navigationExtras = { state: { launchPaymentModalAutomatically: true }, }; await this.router.navigate( - ["organizations", organizationId, "billing", route], + ["organizations", organizationId, "billing", "payment-details"], navigationExtras, ); } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index 31e56836375..724e5891dc8 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -39,12 +39,7 @@ *ngIf="canAccessBilling$ | async" > - @if (managePaymentDetailsOutsideCheckout$ | async) { - - } + ; protected clientsTranslationKey$: Observable; - protected managePaymentDetailsOutsideCheckout$: Observable; protected providerPortalTakeover$: Observable; protected subscriber$: Observable; @@ -100,10 +99,6 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy { ), ); - this.managePaymentDetailsOutsideCheckout$ = this.configService.getFeatureFlag$( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - this.provider$ .pipe( switchMap((provider) => diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 263b90f5b32..24e8a757bdf 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -7,14 +7,18 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CardComponent, ScrollLayoutDirective, SearchModule } from "@bitwarden/components"; import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component"; import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; -import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component"; -import { VerifyBankAccountComponent } from "@bitwarden/web-vault/app/billing/shared/verify-bank-account/verify-bank-account.component"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, +} from "@bitwarden/web-vault/app/billing/payment/components"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; import { CreateClientDialogComponent, + InvoicesComponent, ManageClientNameDialogComponent, ManageClientSubscriptionDialogComponent, + NoInvoicesComponent, ProviderBillingHistoryComponent, ProviderSubscriptionComponent, ProviderSubscriptionStatusComponent, @@ -52,11 +56,11 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr ProvidersLayoutComponent, DangerZoneComponent, ScrollingModule, - VerifyBankAccountComponent, CardComponent, ScrollLayoutDirective, - PaymentComponent, ProviderWarningsModule, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, ], declarations: [ AcceptProviderComponent, @@ -72,8 +76,10 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr AddEditMemberDialogComponent, AddExistingOrganizationDialogComponent, CreateClientDialogComponent, + InvoicesComponent, ManageClientNameDialogComponent, ManageClientSubscriptionDialogComponent, + NoInvoicesComponent, ProviderBillingHistoryComponent, ProviderSubscriptionComponent, ProviderSubscriptionStatusComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html index daae7e2ed2e..43f49b1fd04 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -29,13 +29,11 @@

{{ "paymentMethod" | i18n }}

- - + + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index 72ca0bc8391..0fa69c7a0e6 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -1,25 +1,24 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, Subject, switchMap } from "rxjs"; import { first, takeUntil } from "rxjs/operators"; -import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ProviderKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, + getBillingAddressFromForm, +} from "@bitwarden/web-vault/app/billing/payment/components"; @Component({ selector: "provider-setup", @@ -27,16 +26,17 @@ import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/paymen standalone: false, }) export class SetupComponent implements OnInit, OnDestroy { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxInformationComponent: ManageTaxInformationComponent; + @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; loading = true; - providerId: string; - token: string; + providerId!: string; + token!: string; protected formGroup = this.formBuilder.group({ name: ["", Validators.required], billingEmail: ["", [Validators.required, Validators.email]], + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), }); private destroy$ = new Subject(); @@ -69,7 +69,7 @@ export class SetupComponent implements OnInit, OnDestroy { if (error) { this.toastService.showToast({ variant: "error", - title: null, + title: "", message: this.i18nService.t("emergencyInviteAcceptFailed"), timeout: 10000, }); @@ -95,6 +95,7 @@ export class SetupComponent implements OnInit, OnDestroy { replaceUrl: true, }); } + this.loading = false; } catch (error) { this.validationService.showError(error); @@ -115,10 +116,7 @@ export class SetupComponent implements OnInit, OnDestroy { try { this.formGroup.markAllAsTouched(); - const paymentValid = this.paymentComponent.validate(); - const taxInformationValid = this.taxInformationComponent.validate(); - - if (!paymentValid || !taxInformationValid || !this.formGroup.valid) { + if (this.formGroup.invalid) { return; } const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); @@ -126,29 +124,24 @@ export class SetupComponent implements OnInit, OnDestroy { const key = providerKey[0].encryptedString; const request = new ProviderSetupRequest(); - request.name = this.formGroup.value.name; - request.billingEmail = this.formGroup.value.billingEmail; + request.name = this.formGroup.value.name!; + request.billingEmail = this.formGroup.value.billingEmail!; request.token = this.token; - request.key = key; + request.key = key!; - request.taxInfo = new ExpandedTaxInfoUpdateRequest(); - const taxInformation = this.taxInformationComponent.getTaxInformation(); + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + if (!paymentMethod) { + return; + } - request.taxInfo.country = taxInformation.country; - request.taxInfo.postalCode = taxInformation.postalCode; - request.taxInfo.taxId = taxInformation.taxId; - request.taxInfo.line1 = taxInformation.line1; - request.taxInfo.line2 = taxInformation.line2; - request.taxInfo.city = taxInformation.city; - request.taxInfo.state = taxInformation.state; - - request.paymentSource = await this.paymentComponent.tokenize(); + request.paymentMethod = paymentMethod; + request.billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); const provider = await this.providerApiService.postProviderSetup(this.providerId, request); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("providerSetup"), }); @@ -156,20 +149,10 @@ export class SetupComponent implements OnInit, OnDestroy { await this.router.navigate(["/providers", provider.id]); } catch (e) { - if ( - this.paymentComponent.selected === PaymentMethodType.PayPal && - typeof e === "string" && - e === "No payment method is available." - ) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("clickPayWithPayPal"), - }); - } else { + if (e !== null && typeof e === "object" && "message" in e && typeof e.message === "string") { e.message = this.i18nService.translate(e.message) || e.message; - this.validationService.showError(e); } + this.validationService.showError(e); } }; } diff --git a/libs/angular/src/billing/components/invoices/invoices.component.html b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.html similarity index 100% rename from libs/angular/src/billing/components/invoices/invoices.component.html rename to bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.html diff --git a/libs/angular/src/billing/components/invoices/invoices.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.ts similarity index 100% rename from libs/angular/src/billing/components/invoices/invoices.component.ts rename to bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.ts diff --git a/libs/angular/src/billing/components/invoices/no-invoices.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/no-invoices.component.ts similarity index 100% rename from libs/angular/src/billing/components/invoices/no-invoices.component.ts rename to bitwarden_license/bit-web/src/app/billing/providers/billing-history/no-invoices.component.ts diff --git a/bitwarden_license/bit-web/src/app/billing/providers/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/index.ts index b1294bc8047..3cd83e68990 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/index.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/index.ts @@ -1,3 +1,5 @@ +export * from "./billing-history/invoices.component"; +export * from "./billing-history/no-invoices.component"; export * from "./billing-history/provider-billing-history.component"; export * from "./clients"; export * from "./guards/has-consolidated-billing.guard"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts index d2ac2cede2f..5a070687de4 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts @@ -1,13 +1,10 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute } from "@angular/router"; import { BehaviorSubject, combineLatest, - EMPTY, filter, firstValueFrom, - from, - map, merge, Observable, of, @@ -19,7 +16,6 @@ import { tap, withLatestFrom, } from "rxjs"; -import { catchError } from "rxjs/operators"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; @@ -49,13 +45,6 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { ProviderWarningsService } from "../warnings/services"; -class RedirectError { - constructor( - public path: string[], - public relativeTo: ActivatedRoute, - ) {} -} - type View = { activeUserId: UserId; provider: BitwardenSubscriber; @@ -92,18 +81,6 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy { ); private load$: Observable = this.provider$.pipe( - switchMap((provider) => - this.configService - .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) - .pipe( - map((managePaymentDetailsOutsideCheckout) => { - if (!managePaymentDetailsOutsideCheckout) { - throw new RedirectError(["../subscription"], this.activatedRoute); - } - return provider; - }), - ), - ), mapProviderToSubscriber, switchMap(async (provider) => { const getTaxIdWarning = firstValueFrom( @@ -131,14 +108,6 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy { }; }), shareReplay({ bufferSize: 1, refCount: false }), - catchError((error: unknown) => { - if (error instanceof RedirectError) { - return from(this.router.navigate(error.path, { relativeTo: error.relativeTo })).pipe( - switchMap(() => EMPTY), - ); - } - throw error; - }), ); view$: Observable = merge( @@ -158,7 +127,6 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy { private messageListener: MessageListener, private providerService: ProviderService, private providerWarningsService: ProviderWarningsService, - private router: Router, private subscriberBillingClient: SubscriberBillingClient, ) {} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html index 0205d2838d1..05eda7e7ea4 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html @@ -62,51 +62,5 @@
- @if (!managePaymentDetailsOutsideCheckout) { - - -

- {{ "accountCredit" | i18n }} -

-

{{ subscription.accountCredit | currency: "$" }}

-

{{ "creditAppliedDesc" | i18n }}

-
- - -

{{ "paymentMethod" | i18n }}

-

- {{ "noPaymentMethod" | i18n }} -

- - - -

- - {{ subscription.paymentSource.description }} - - {{ "unverified" | i18n }} -

-
- -
- - -

{{ "taxInformation" | i18n }}

-

{{ "taxInformationDesc" | i18n }}

- -
- } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts index 83a23760d80..98aceb0f878 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts @@ -2,26 +2,14 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, lastValueFrom, Subject, takeUntil } from "rxjs"; +import { concatMap, Subject, takeUntil } from "rxjs"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { TaxInformation } from "@bitwarden/common/billing/models/domain"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; import { ProviderPlanResponse, ProviderSubscriptionResponse, } from "@bitwarden/common/billing/models/response/provider-subscription-response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, ToastService } from "@bitwarden/components"; import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service"; -import { - AdjustPaymentDialogComponent, - AdjustPaymentDialogResultType, -} from "@bitwarden/web-vault/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component"; @Component({ selector: "app-provider-subscription", @@ -36,18 +24,11 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { protected loading: boolean; private destroy$ = new Subject(); protected totalCost: number; - protected managePaymentDetailsOutsideCheckout: boolean; - - protected readonly TaxInformation = TaxInformation; constructor( private billingApiService: BillingApiServiceAbstraction, - private i18nService: I18nService, private route: ActivatedRoute, private billingNotificationService: BillingNotificationService, - private dialogService: DialogService, - private toastService: ToastService, - private configService: ConfigService, ) {} async ngOnInit() { @@ -55,9 +36,6 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { .pipe( concatMap(async (params) => { this.providerId = params.providerId; - this.managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); await this.load(); this.firstLoaded = true; }), @@ -83,40 +61,6 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { } } - protected updatePaymentMethod = async (): Promise => { - const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { - data: { - initialPaymentMethod: this.subscription.paymentSource?.type, - providerId: this.providerId, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustPaymentDialogResultType.Submitted) { - await this.load(); - } - }; - - protected updateTaxInformation = async (taxInformation: TaxInformation) => { - try { - const request = ExpandedTaxInfoUpdateRequest.From(taxInformation); - await this.billingApiService.updateProviderTaxInformation(this.providerId, request); - this.billingNotificationService.showSuccess(this.i18nService.t("updatedTaxInformation")); - } catch (error) { - this.billingNotificationService.handleError(error); - } - }; - - protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise => { - await this.billingApiService.verifyProviderBankAccount(this.providerId, request); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("verifiedBankAccount"), - }); - }; - protected getFormattedCost( cost: number, seatMinimum: number, @@ -161,7 +105,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { } protected getBillingCadenceLabel(providerPlanResponse: ProviderPlanResponse): string { - if (providerPlanResponse == null || providerPlanResponse == undefined) { + if (providerPlanResponse == null) { return "month"; } @@ -174,27 +118,4 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { return "month"; } } - - protected get paymentSourceClasses() { - if (this.subscription.paymentSource == null) { - return []; - } - switch (this.subscription.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - case PaymentMethodType.Check: - return ["bwi-billing"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } - } - - protected get updatePaymentSourceButtonText(): string { - const key = - this.subscription.paymentSource == null ? "addPaymentMethod" : "changePaymentMethod"; - return this.i18nService.t(key); - } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/warnings/types/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/warnings/types/index.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index d9ff3ec5619..e301c0462c3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -23,8 +23,6 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -117,7 +115,6 @@ export class OverviewComponent implements OnInit, OnDestroy { private smOnboardingTasksService: SMOnboardingTasksService, private logService: LogService, private router: Router, - private configService: ConfigService, ) {} ngOnInit() { @@ -218,13 +215,12 @@ export class OverviewComponent implements OnInit, OnDestroy { } async navigateToPaymentMethod() { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, + await this.router.navigate( + ["organizations", `${this.organizationId}`, "billing", "payment-details"], + { + state: { launchPaymentModalAutomatically: true }, + }, ); - const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; - await this.router.navigate(["organizations", `${this.organizationId}`, "billing", route], { - state: { launchPaymentModalAutomatically: true }, - }); } ngOnDestroy(): void { diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html deleted file mode 100644 index c9c0c296ada..00000000000 --- a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html +++ /dev/null @@ -1,55 +0,0 @@ -
- - -

{{ "creditDelayed" | i18n }}

-
- - - {{ "payPal" | i18n }} - - - {{ "bitcoin" | i18n }} - - -
-
- - {{ "amount" | i18n }} - - $USD - -
-
- - - - -
-
-
- - - - - - - - - - - - - - - -
diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts deleted file mode 100644 index 3dc56c55b0c..00000000000 --- a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts +++ /dev/null @@ -1,167 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { AccountService, AccountInfo } from "@bitwarden/common/auth/abstractions/account.service"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { UserId } from "@bitwarden/common/types/guid"; -import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; - -export type AddAccountCreditDialogParams = { - organizationId?: string; - providerId?: string; -}; - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum AddAccountCreditDialogResultType { - Closed = "closed", - Submitted = "submitted", -} - -export const openAddAccountCreditDialog = ( - dialogService: DialogService, - dialogConfig: DialogConfig, -) => - dialogService.open( - AddAccountCreditDialogComponent, - dialogConfig, - ); - -type PayPalConfig = { - businessId?: string; - buttonAction?: string; - returnUrl?: string; - customField?: string; - subject?: string; -}; - -@Component({ - templateUrl: "./add-account-credit-dialog.component.html", - standalone: false, -}) -export class AddAccountCreditDialogComponent implements OnInit { - @ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm: ElementRef; - protected formGroup = new FormGroup({ - paymentMethod: new FormControl(PaymentMethodType.PayPal), - creditAmount: new FormControl(null, [Validators.required, Validators.min(0.01)]), - }); - protected payPalConfig: PayPalConfig; - protected ResultType = AddAccountCreditDialogResultType; - - private organization?: Organization; - private provider?: Provider; - private user?: { id: UserId } & AccountInfo; - - constructor( - private accountService: AccountService, - private apiService: ApiService, - private configService: ConfigService, - @Inject(DIALOG_DATA) private dialogParams: AddAccountCreditDialogParams, - private dialogRef: DialogRef, - private organizationService: OrganizationService, - private platformUtilsService: PlatformUtilsService, - private providerService: ProviderService, - ) { - this.payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig; - } - - protected readonly paymentMethodType = PaymentMethodType; - - submit = async () => { - this.formGroup.markAllAsTouched(); - - if (this.formGroup.invalid) { - return; - } - - if (this.formGroup.value.paymentMethod === PaymentMethodType.PayPal) { - this.payPalForm.nativeElement.submit(); - return; - } - - if (this.formGroup.value.paymentMethod === PaymentMethodType.BitPay) { - const request = this.getBitPayInvoiceRequest(); - const bitPayUrl = await this.apiService.postBitPayInvoice(request); - this.platformUtilsService.launchUri(bitPayUrl); - return; - } - - this.dialogRef.close(AddAccountCreditDialogResultType.Submitted); - }; - - async ngOnInit(): Promise { - let payPalCustomField: string; - - if (this.dialogParams.organizationId) { - this.formGroup.patchValue({ - creditAmount: 20.0, - }); - this.user = await firstValueFrom(this.accountService.activeAccount$); - this.organization = await firstValueFrom( - this.organizationService - .organizations$(this.user.id) - .pipe( - map((organizations) => - organizations.find((org) => org.id === this.dialogParams.organizationId), - ), - ), - ); - payPalCustomField = "organization_id:" + this.organization.id; - this.payPalConfig.subject = this.organization.name; - } else if (this.dialogParams.providerId) { - this.formGroup.patchValue({ - creditAmount: 20.0, - }); - this.provider = await firstValueFrom( - this.providerService.get$(this.dialogParams.providerId, this.user.id), - ); - payPalCustomField = "provider_id:" + this.provider.id; - this.payPalConfig.subject = this.provider.name; - } else { - this.formGroup.patchValue({ - creditAmount: 10.0, - }); - payPalCustomField = "user_id:" + this.user.id; - this.payPalConfig.subject = this.user.email; - } - - const region = await firstValueFrom(this.configService.cloudRegion$); - - payPalCustomField += ",account_credit:1"; - payPalCustomField += `,region:${region}`; - - this.payPalConfig.customField = payPalCustomField; - this.payPalConfig.returnUrl = window.location.href; - } - - getBitPayInvoiceRequest(): BitPayInvoiceRequest { - const request = new BitPayInvoiceRequest(); - if (this.organization) { - request.name = this.organization.name; - request.organizationId = this.organization.id; - } else if (this.provider) { - request.name = this.provider.name; - request.providerId = this.provider.id; - } else { - request.email = this.user.email; - request.userId = this.user.id; - } - - request.credit = true; - request.amount = this.formGroup.value.creditAmount; - request.returnUrl = window.location.href; - - return request; - } -} diff --git a/libs/angular/src/billing/components/index.ts b/libs/angular/src/billing/components/index.ts index dacb5b265bd..34e1d27c1ed 100644 --- a/libs/angular/src/billing/components/index.ts +++ b/libs/angular/src/billing/components/index.ts @@ -1,5 +1 @@ -export * from "./add-account-credit-dialog/add-account-credit-dialog.component"; -export * from "./invoices/invoices.component"; -export * from "./invoices/no-invoices.component"; -export * from "./manage-tax-information/manage-tax-information.component"; export * from "./premium.component"; diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html deleted file mode 100644 index 391765251b0..00000000000 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html +++ /dev/null @@ -1,90 +0,0 @@ -
-
-
- - {{ "country" | i18n }} - - - - -
-
- - {{ "zipPostalCode" | i18n }} - - -
- -
- - {{ "address1" | i18n }} - - -
-
- - {{ "address2" | i18n }} - - -
-
- - {{ "cityTown" | i18n }} - - -
-
- - {{ "stateProvince" | i18n }} - - -
-
- - {{ "taxIdNumber" | i18n }} - - -
-
-
- -
-
-
diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.spec.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.spec.ts deleted file mode 100644 index c662e20b275..00000000000 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.spec.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { SimpleChange } from "@angular/core"; -import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { ReactiveFormsModule } from "@angular/forms"; -import { mock, MockProxy } from "jest-mock-extended"; - -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { SelectModule, FormFieldModule, BitSubmitDirective } from "@bitwarden/components"; -import { I18nPipe } from "@bitwarden/ui-common"; - -import { ManageTaxInformationComponent } from "./manage-tax-information.component"; - -describe("ManageTaxInformationComponent", () => { - let sut: ManageTaxInformationComponent; - let fixture: ComponentFixture; - let mockTaxService: MockProxy; - - beforeEach(async () => { - mockTaxService = mock(); - await TestBed.configureTestingModule({ - declarations: [ManageTaxInformationComponent], - providers: [ - { provide: TaxServiceAbstraction, useValue: mockTaxService }, - { provide: I18nService, useValue: { t: (key: string) => key } }, - ], - imports: [ - CommonModule, - ReactiveFormsModule, - SelectModule, - FormFieldModule, - BitSubmitDirective, - I18nPipe, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(ManageTaxInformationComponent); - sut = fixture.componentInstance; - fixture.autoDetectChanges(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it("creates successfully", () => { - expect(sut).toBeTruthy(); - }); - - it("should initialize with all values empty in startWith", async () => { - // Arrange - sut.startWith = { - country: "", - postalCode: "", - taxId: "", - line1: "", - line2: "", - city: "", - state: "", - }; - - // Act - fixture.detectChanges(); - - // Assert - const startWithValue = sut.startWith; - expect(startWithValue.line1).toHaveLength(0); - expect(startWithValue.line2).toHaveLength(0); - expect(startWithValue.city).toHaveLength(0); - expect(startWithValue.state).toHaveLength(0); - expect(startWithValue.postalCode).toHaveLength(0); - expect(startWithValue.country).toHaveLength(0); - expect(startWithValue.taxId).toHaveLength(0); - }); - - it("should update the tax information protected state when form is updated", async () => { - // Arrange - const line1Value = "123 Street"; - const line2Value = "Apt. 5"; - const cityValue = "New York"; - const stateValue = "NY"; - const countryValue = "USA"; - const postalCodeValue = "123 Street"; - - sut.startWith = { - country: countryValue, - postalCode: "", - taxId: "", - line1: "", - line2: "", - city: "", - state: "", - }; - sut.showTaxIdField = false; - mockTaxService.isCountrySupported.mockResolvedValue(true); - - // Act - await sut.ngOnInit(); - fixture.detectChanges(); - - const line1: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='line1']", - ); - const line2: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='line2']", - ); - const city: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='city']", - ); - const state: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='state']", - ); - const postalCode: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='postalCode']", - ); - - line1.value = line1Value; - line2.value = line2Value; - city.value = cityValue; - state.value = stateValue; - postalCode.value = postalCodeValue; - - line1.dispatchEvent(new Event("input")); - line2.dispatchEvent(new Event("input")); - city.dispatchEvent(new Event("input")); - state.dispatchEvent(new Event("input")); - postalCode.dispatchEvent(new Event("input")); - await fixture.whenStable(); - - // Assert - - // Assert that the internal tax information reflects the form - const taxInformation = sut.getTaxInformation(); - expect(taxInformation.line1).toBe(line1Value); - expect(taxInformation.line2).toBe(line2Value); - expect(taxInformation.city).toBe(cityValue); - expect(taxInformation.state).toBe(stateValue); - expect(taxInformation.postalCode).toBe(postalCodeValue); - expect(taxInformation.country).toBe(countryValue); - expect(taxInformation.taxId).toHaveLength(0); - - expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); - expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(2); - }); - - it("should not show address fields except postal code if country is not supported for taxes", async () => { - // Arrange - const countryValue = "UNKNOWN"; - sut.startWith = { - country: countryValue, - postalCode: "", - taxId: "", - line1: "", - line2: "", - city: "", - state: "", - }; - sut.showTaxIdField = false; - mockTaxService.isCountrySupported.mockResolvedValue(false); - - // Act - await sut.ngOnInit(); - fixture.detectChanges(); - - const line1: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='line1']", - ); - const line2: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='line2']", - ); - const city: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='city']", - ); - const state: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='state']", - ); - const postalCode: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='postalCode']", - ); - - // Assert - expect(line1).toBeNull(); - expect(line2).toBeNull(); - expect(city).toBeNull(); - expect(state).toBeNull(); - //Should be visible - expect(postalCode).toBeTruthy(); - - expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); - expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1); - }); - - it("should not show the tax id field if showTaxIdField is set to false", async () => { - // Arrange - const countryValue = "USA"; - sut.startWith = { - country: countryValue, - postalCode: "", - taxId: "", - line1: "", - line2: "", - city: "", - state: "", - }; - - sut.showTaxIdField = false; - mockTaxService.isCountrySupported.mockResolvedValue(true); - - // Act - await sut.ngOnInit(); - fixture.detectChanges(); - - // Assert - const taxId: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='taxId']", - ); - expect(taxId).toBeNull(); - - expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); - expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1); - }); - - it("should clear the tax id field if showTaxIdField is set to false after being true", async () => { - // Arrange - const countryValue = "USA"; - const taxIdValue = "A12345678"; - - sut.startWith = { - country: countryValue, - postalCode: "", - taxId: taxIdValue, - line1: "", - line2: "", - city: "", - state: "", - }; - sut.showTaxIdField = true; - - mockTaxService.isCountrySupported.mockResolvedValue(true); - await sut.ngOnInit(); - fixture.detectChanges(); - const initialTaxIdValue = fixture.nativeElement.querySelector( - "input[formControlName='taxId']", - ).value; - - // Act - sut.showTaxIdField = false; - sut.ngOnChanges({ showTaxIdField: new SimpleChange(true, false, false) }); - fixture.detectChanges(); - - // Assert - const taxId = fixture.nativeElement.querySelector("input[formControlName='taxId']"); - expect(taxId).toBeNull(); - - const taxInformation = sut.getTaxInformation(); - expect(taxInformation.taxId).toBeNull(); - expect(initialTaxIdValue).toEqual(taxIdValue); - - expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); - expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1); - }); -}); diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts deleted file mode 100644 index 0b87f3f931d..00000000000 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts +++ /dev/null @@ -1,166 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { - Component, - EventEmitter, - Input, - OnChanges, - OnDestroy, - OnInit, - Output, - SimpleChanges, -} from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; -import { debounceTime } from "rxjs/operators"; - -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/models/domain"; - -@Component({ - selector: "app-manage-tax-information", - templateUrl: "./manage-tax-information.component.html", - standalone: false, -}) -export class ManageTaxInformationComponent implements OnInit, OnDestroy, OnChanges { - @Input() startWith: TaxInformation; - @Input() onSubmit?: (taxInformation: TaxInformation) => Promise; - @Input() showTaxIdField: boolean = true; - - /** - * Emits when the tax information has changed. - */ - @Output() taxInformationChanged = new EventEmitter(); - - /** - * Emits when the tax information has been updated. - */ - @Output() taxInformationUpdated = new EventEmitter(); - - private taxInformation: TaxInformation; - - protected formGroup = this.formBuilder.group({ - country: ["", Validators.required], - postalCode: ["", Validators.required], - taxId: "", - line1: "", - line2: "", - city: "", - state: "", - }); - - protected isTaxSupported: boolean; - - private destroy$ = new Subject(); - - protected readonly countries: CountryListItem[] = this.taxService.getCountries(); - - constructor( - private formBuilder: FormBuilder, - private taxService: TaxServiceAbstraction, - ) {} - - getTaxInformation(): TaxInformation { - return this.taxInformation; - } - - submit = async () => { - this.markAllAsTouched(); - if (this.formGroup.invalid) { - return; - } - await this.onSubmit?.(this.taxInformation); - this.taxInformationUpdated.emit(); - }; - - validate(): boolean { - this.markAllAsTouched(); - return this.formGroup.valid; - } - - markAllAsTouched() { - this.formGroup.markAllAsTouched(); - } - - async ngOnInit() { - this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((values) => { - this.taxInformation = { - country: values.country, - postalCode: values.postalCode, - taxId: values.taxId, - line1: values.line1, - line2: values.line2, - city: values.city, - state: values.state, - }; - }); - - if (this.startWith) { - this.formGroup.controls.country.setValue(this.startWith.country); - this.formGroup.controls.postalCode.setValue(this.startWith.postalCode); - - this.isTaxSupported = - this.startWith && this.startWith.country - ? await this.taxService.isCountrySupported(this.startWith.country) - : false; - - if (this.isTaxSupported) { - this.formGroup.controls.taxId.setValue(this.startWith.taxId); - this.formGroup.controls.line1.setValue(this.startWith.line1); - this.formGroup.controls.line2.setValue(this.startWith.line2); - this.formGroup.controls.city.setValue(this.startWith.city); - this.formGroup.controls.state.setValue(this.startWith.state); - } - } - - this.formGroup.controls.country.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe((country: string) => { - this.taxService - .isCountrySupported(country) - .then((isSupported) => (this.isTaxSupported = isSupported)) - .catch(() => (this.isTaxSupported = false)) - .finally(() => { - if (!this.isTaxSupported) { - this.formGroup.controls.taxId.setValue(null); - this.formGroup.controls.line1.setValue(null); - this.formGroup.controls.line2.setValue(null); - this.formGroup.controls.city.setValue(null); - this.formGroup.controls.state.setValue(null); - } - if (this.taxInformationChanged) { - this.taxInformationChanged.emit(this.taxInformation); - } - }); - }); - - this.formGroup.controls.postalCode.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - if (this.taxInformationChanged) { - this.taxInformationChanged.emit(this.taxInformation); - } - }); - - this.formGroup.controls.taxId.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - if (this.taxInformationChanged) { - this.taxInformationChanged.emit(this.taxInformation); - } - }); - } - - ngOnChanges(changes: SimpleChanges): void { - // Clear the value of the tax-id if states have been changed in the parent component - const showTaxIdField = changes["showTaxIdField"]; - if (showTaxIdField && !showTaxIdField.currentValue) { - this.formGroup.controls.taxId.setValue(null); - } - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } -} diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index c0bf1425d47..446530a1111 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -2,12 +2,6 @@ import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { - AddAccountCreditDialogComponent, - InvoicesComponent, - NoInvoicesComponent, - ManageTaxInformationComponent, -} from "@bitwarden/angular/billing/components"; import { AsyncActionsModule, AutofocusDirective, @@ -112,10 +106,6 @@ import { IconComponent } from "./vault/components/icon.component"; UserTypePipe, IfFeatureDirective, FingerprintPipe, - AddAccountCreditDialogComponent, - InvoicesComponent, - NoInvoicesComponent, - ManageTaxInformationComponent, TwoFactorIconComponent, ], exports: [ @@ -146,10 +136,6 @@ import { IconComponent } from "./vault/components/icon.component"; UserTypePipe, IfFeatureDirective, FingerprintPipe, - AddAccountCreditDialogComponent, - InvoicesComponent, - NoInvoicesComponent, - ManageTaxInformationComponent, TwoFactorIconComponent, TextDragDirective, ], diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 03d756ee11c..8c727a98d11 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -144,14 +144,12 @@ import { AccountBillingApiServiceAbstraction } from "@bitwarden/common/billing/a import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; import { OrganizationSponsorshipApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { AccountBillingApiService } from "@bitwarden/common/billing/services/account/account-billing-api.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service"; import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service"; import { OrganizationSponsorshipApiService } from "@bitwarden/common/billing/services/organization/organization-sponsorship-api.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; -import { TaxService } from "@bitwarden/common/billing/services/tax.service"; import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service"; import { DefaultKeyGenerationService, @@ -1398,11 +1396,6 @@ const safeProviders: SafeProvider[] = [ useClass: BillingApiService, deps: [ApiServiceAbstraction], }), - safeProvider({ - provide: TaxServiceAbstraction, - useClass: TaxService, - deps: [ApiServiceAbstraction], - }), safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index d746342d728..28ab0613e14 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -77,14 +77,10 @@ import { } from "../auth/models/response/two-factor-web-authn.response"; import { TwoFactorYubiKeyResponse } from "../auth/models/response/two-factor-yubi-key.response"; import { BitPayInvoiceRequest } from "../billing/models/request/bit-pay-invoice.request"; -import { PaymentRequest } from "../billing/models/request/payment.request"; -import { TaxInfoUpdateRequest } from "../billing/models/request/tax-info-update.request"; import { BillingHistoryResponse } from "../billing/models/response/billing-history.response"; -import { BillingPaymentResponse } from "../billing/models/response/billing-payment.response"; import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; -import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request"; import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request"; import { DeleteRecoverRequest } from "../models/request/delete-recover.request"; @@ -171,10 +167,8 @@ export abstract class ApiService { abstract getProfile(): Promise; abstract getUserSubscription(): Promise; - abstract getTaxInfo(): Promise; abstract putProfile(request: UpdateProfileRequest): Promise; abstract putAvatar(request: UpdateAvatarRequest): Promise; - abstract putTaxInfo(request: TaxInfoUpdateRequest): Promise; abstract postPrelogin(request: PreloginRequest): Promise; abstract postEmailToken(request: EmailTokenRequest): Promise; abstract postEmail(request: EmailRequest): Promise; @@ -185,7 +179,6 @@ export abstract class ApiService { abstract postPremium(data: FormData): Promise; abstract postReinstatePremium(): Promise; abstract postAccountStorage(request: StorageRequest): Promise; - abstract postAccountPayment(request: PaymentRequest): Promise; abstract postAccountLicense(data: FormData): Promise; abstract postAccountKeys(request: KeysRequest): Promise; abstract postAccountVerifyEmail(): Promise; @@ -209,7 +202,6 @@ export abstract class ApiService { abstract getLastAuthRequest(): Promise; abstract getUserBillingHistory(): Promise; - abstract getUserBillingPayment(): Promise; abstract getCipher(id: string): Promise; abstract getFullCipherDetails(id: string): Promise; diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index 10626d6758f..6c91c2ea0cf 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -3,21 +3,17 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; -import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request"; import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request"; import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; -import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; -import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response"; import { ImportDirectoryRequest } from "../../../models/request/import-directory.request"; import { SeatRequest } from "../../../models/request/seat.request"; import { StorageRequest } from "../../../models/request/storage.request"; -import { VerifyBankRequest } from "../../../models/request/verify-bank.request"; import { ListResponse } from "../../../models/response/list.response"; import { OrganizationApiKeyType } from "../../enums"; import { OrganizationCollectionManagementUpdateRequest } from "../../models/request/organization-collection-management-update.request"; @@ -45,7 +41,6 @@ export abstract class OrganizationApiServiceAbstraction { ): Promise; abstract createLicense(data: FormData): Promise; abstract save(id: string, request: OrganizationUpdateRequest): Promise; - abstract updatePayment(id: string, request: PaymentRequest): Promise; abstract upgrade(id: string, request: OrganizationUpgradeRequest): Promise; abstract updatePasswordManagerSeats( id: string, @@ -57,7 +52,6 @@ export abstract class OrganizationApiServiceAbstraction { ): Promise; abstract updateSeats(id: string, request: SeatRequest): Promise; abstract updateStorage(id: string, request: StorageRequest): Promise; - abstract verifyBank(id: string, request: VerifyBankRequest): Promise; abstract reinstate(id: string): Promise; abstract leave(id: string): Promise; abstract delete(id: string, request: SecretVerificationRequest): Promise; @@ -76,8 +70,6 @@ export abstract class OrganizationApiServiceAbstraction { organizationApiKeyType?: OrganizationApiKeyType, ): Promise>; abstract rotateApiKey(id: string, request: OrganizationApiKeyRequest): Promise; - abstract getTaxInfo(id: string): Promise; - abstract updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise; abstract getKeys(id: string): Promise; abstract updateKeys( id: string, diff --git a/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts b/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts index 5c9ea5526a0..001bba11cf4 100644 --- a/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts +++ b/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts @@ -1,7 +1,19 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ExpandedTaxInfoUpdateRequest } from "../../../../billing/models/request/expanded-tax-info-update.request"; -import { TokenizedPaymentSourceRequest } from "../../../../billing/models/request/tokenized-payment-source.request"; +interface TokenizedPaymentMethod { + type: "bankAccount" | "card" | "payPal"; + token: string; +} + +interface BillingAddress { + country: string; + postalCode: string; + line1: string | null; + line2: string | null; + city: string | null; + state: string | null; + taxId: { code: string; value: string } | null; +} export class ProviderSetupRequest { name: string; @@ -9,6 +21,6 @@ export class ProviderSetupRequest { billingEmail: string; token: string; key: string; - taxInfo: ExpandedTaxInfoUpdateRequest; - paymentSource?: TokenizedPaymentSourceRequest; + paymentMethod: TokenizedPaymentMethod; + billingAddress: BillingAddress; } diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index 598bb2a29db..6a7b71389bb 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -7,21 +7,17 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; -import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request"; import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request"; import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; -import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; -import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response"; import { ImportDirectoryRequest } from "../../../models/request/import-directory.request"; import { SeatRequest } from "../../../models/request/seat.request"; import { StorageRequest } from "../../../models/request/storage.request"; -import { VerifyBankRequest } from "../../../models/request/verify-bank.request"; import { ListResponse } from "../../../models/response/list.response"; import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction"; @@ -143,10 +139,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return data; } - async updatePayment(id: string, request: PaymentRequest): Promise { - return this.apiService.send("POST", "/organizations/" + id + "/payment", request, true, false); - } - async upgrade(id: string, request: OrganizationUpgradeRequest): Promise { const r = await this.apiService.send( "POST", @@ -208,16 +200,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return new PaymentResponse(r); } - async verifyBank(id: string, request: VerifyBankRequest): Promise { - await this.apiService.send( - "POST", - "/organizations/" + id + "/verify-bank", - request, - true, - false, - ); - } - async reinstate(id: string): Promise { return this.apiService.send("POST", "/organizations/" + id + "/reinstate", null, true, false); } @@ -299,16 +281,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return new ApiKeyResponse(r); } - async getTaxInfo(id: string): Promise { - const r = await this.apiService.send("GET", "/organizations/" + id + "/tax", null, true, true); - return new TaxInfoResponse(r); - } - - async updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise { - // Can't broadcast anything because the response doesn't have content - return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false); - } - async getKeys(id: string): Promise { const r = await this.apiService.send("GET", "/organizations/" + id + "/keys", null, true, true); return new OrganizationKeysResponse(r); diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index 2f3fe9125db..b5695e2e8a0 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -1,19 +1,12 @@ -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; - import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; import { ListResponse } from "../../models/response/list.response"; -import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; -import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request"; import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; -import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request"; -import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request"; import { InvoicesResponse } from "../models/response/invoices.response"; -import { PaymentMethodResponse } from "../models/response/payment-method.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { @@ -29,14 +22,10 @@ export abstract class BillingApiServiceAbstraction { request: CreateClientOrganizationRequest, ): Promise; - abstract createSetupIntent(paymentMethodType: PaymentMethodType): Promise; - abstract getOrganizationBillingMetadata( organizationId: string, ): Promise; - abstract getOrganizationPaymentMethod(organizationId: string): Promise; - abstract getPlans(): Promise>; abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise; @@ -49,44 +38,12 @@ export abstract class BillingApiServiceAbstraction { abstract getProviderSubscription(providerId: string): Promise; - abstract getProviderTaxInformation(providerId: string): Promise; - - abstract updateOrganizationPaymentMethod( - organizationId: string, - request: UpdatePaymentMethodRequest, - ): Promise; - - abstract updateOrganizationTaxInformation( - organizationId: string, - request: ExpandedTaxInfoUpdateRequest, - ): Promise; - abstract updateProviderClientOrganization( providerId: string, organizationId: string, request: UpdateClientOrganizationRequest, ): Promise; - abstract updateProviderPaymentMethod( - providerId: string, - request: UpdatePaymentMethodRequest, - ): Promise; - - abstract updateProviderTaxInformation( - providerId: string, - request: ExpandedTaxInfoUpdateRequest, - ): Promise; - - abstract verifyOrganizationBankAccount( - organizationId: string, - request: VerifyBankAccountRequest, - ): Promise; - - abstract verifyProviderBankAccount( - providerId: string, - request: VerifyBankAccountRequest, - ): Promise; - abstract restartSubscription( organizationId: string, request: OrganizationCreateRequest, diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 3254787457a..215fabfd955 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -3,7 +3,6 @@ import { UserId } from "@bitwarden/user-core"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; -import { PaymentSourceResponse } from "../models/response/payment-source.response"; export type OrganizationInformation = { name: string; @@ -45,8 +44,6 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - abstract getPaymentSource(organizationId: string): Promise; - abstract purchaseSubscription( subscription: SubscriptionInformation, activeUserId: UserId, diff --git a/libs/common/src/billing/abstractions/tax.service.abstraction.ts b/libs/common/src/billing/abstractions/tax.service.abstraction.ts deleted file mode 100644 index c94fbcba652..00000000000 --- a/libs/common/src/billing/abstractions/tax.service.abstraction.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CountryListItem } from "../models/domain"; -import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; -import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; -import { PreviewTaxAmountForOrganizationTrialRequest } from "../models/request/tax"; -import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; - -export abstract class TaxServiceAbstraction { - abstract getCountries(): CountryListItem[]; - - abstract isCountrySupported(country: string): Promise; - - abstract previewIndividualInvoice( - request: PreviewIndividualInvoiceRequest, - ): Promise; - - abstract previewOrganizationInvoice( - request: PreviewOrganizationInvoiceRequest, - ): Promise; - - abstract previewTaxAmountForOrganizationTrial: ( - request: PreviewTaxAmountForOrganizationTrialRequest, - ) => Promise; -} diff --git a/libs/common/src/billing/enums/bitwarden-product-type.enum.ts b/libs/common/src/billing/enums/bitwarden-product-type.enum.ts deleted file mode 100644 index 4389d283c00..00000000000 --- a/libs/common/src/billing/enums/bitwarden-product-type.enum.ts +++ /dev/null @@ -1,6 +0,0 @@ -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum BitwardenProductType { - PasswordManager = 0, - SecretsManager = 1, -} diff --git a/libs/common/src/billing/enums/index.ts b/libs/common/src/billing/enums/index.ts index 1a9f3f8219c..ee8cd1f5948 100644 --- a/libs/common/src/billing/enums/index.ts +++ b/libs/common/src/billing/enums/index.ts @@ -2,7 +2,6 @@ export * from "./payment-method-type.enum"; export * from "./plan-sponsorship-type.enum"; export * from "./plan-type.enum"; export * from "./transaction-type.enum"; -export * from "./bitwarden-product-type.enum"; export * from "./product-tier-type.enum"; export * from "./product-type.enum"; export * from "./plan-interval.enum"; diff --git a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts deleted file mode 100644 index 83b254ac512..00000000000 --- a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts +++ /dev/null @@ -1,29 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { TaxInformation } from "../domain/tax-information"; - -import { TaxInfoUpdateRequest } from "./tax-info-update.request"; - -export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest { - taxId: string; - line1: string; - line2: string; - city: string; - state: string; - - static From(taxInformation: TaxInformation): ExpandedTaxInfoUpdateRequest { - if (!taxInformation) { - return null; - } - - const request = new ExpandedTaxInfoUpdateRequest(); - request.country = taxInformation.country; - request.postalCode = taxInformation.postalCode; - request.taxId = taxInformation.taxId; - request.line1 = taxInformation.line1; - request.line2 = taxInformation.line2; - request.city = taxInformation.city; - request.state = taxInformation.state; - return request; - } -} diff --git a/libs/common/src/billing/models/request/payment.request.ts b/libs/common/src/billing/models/request/payment.request.ts deleted file mode 100644 index e2edd9aabb3..00000000000 --- a/libs/common/src/billing/models/request/payment.request.ts +++ /dev/null @@ -1,10 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { PaymentMethodType } from "../../enums"; - -import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request"; - -export class PaymentRequest extends ExpandedTaxInfoUpdateRequest { - paymentMethodType: PaymentMethodType; - paymentToken: string; -} diff --git a/libs/common/src/billing/models/request/preview-individual-invoice.request.ts b/libs/common/src/billing/models/request/preview-individual-invoice.request.ts deleted file mode 100644 index f817398c629..00000000000 --- a/libs/common/src/billing/models/request/preview-individual-invoice.request.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-strict-ignore -export class PreviewIndividualInvoiceRequest { - passwordManager: PasswordManager; - taxInformation: TaxInformation; - - constructor(passwordManager: PasswordManager, taxInformation: TaxInformation) { - this.passwordManager = passwordManager; - this.taxInformation = taxInformation; - } -} - -class PasswordManager { - additionalStorage: number; - - constructor(additionalStorage: number) { - this.additionalStorage = additionalStorage; - } -} - -class TaxInformation { - postalCode: string; - country: string; - - constructor(postalCode: string, country: string) { - this.postalCode = postalCode; - this.country = country; - } -} diff --git a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts deleted file mode 100644 index bfeecb4eb23..00000000000 --- a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { PlanSponsorshipType, PlanType } from "../../enums"; - -export class PreviewOrganizationInvoiceRequest { - organizationId?: string; - passwordManager: PasswordManager; - secretsManager?: SecretsManager; - taxInformation: TaxInformation; - - constructor( - passwordManager: PasswordManager, - taxInformation: TaxInformation, - organizationId?: string, - secretsManager?: SecretsManager, - ) { - this.organizationId = organizationId; - this.passwordManager = passwordManager; - this.secretsManager = secretsManager; - this.taxInformation = taxInformation; - } -} - -class PasswordManager { - plan: PlanType; - sponsoredPlan?: PlanSponsorshipType; - seats: number; - additionalStorage: number; - - constructor(plan: PlanType, seats: number, additionalStorage: number) { - this.plan = plan; - this.seats = seats; - this.additionalStorage = additionalStorage; - } -} - -class SecretsManager { - seats: number; - additionalMachineAccounts: number; - - constructor(seats: number, additionalMachineAccounts: number) { - this.seats = seats; - this.additionalMachineAccounts = additionalMachineAccounts; - } -} - -class TaxInformation { - postalCode: string; - country: string; - taxId: string; - - constructor(postalCode: string, country: string, taxId: string) { - this.postalCode = postalCode; - this.country = country; - this.taxId = taxId; - } -} diff --git a/libs/common/src/billing/models/request/tax-info-update.request.ts b/libs/common/src/billing/models/request/tax-info-update.request.ts deleted file mode 100644 index 6f767535472..00000000000 --- a/libs/common/src/billing/models/request/tax-info-update.request.ts +++ /dev/null @@ -1,6 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -export class TaxInfoUpdateRequest { - country: string; - postalCode: string; -} diff --git a/libs/common/src/billing/models/request/tax/index.ts b/libs/common/src/billing/models/request/tax/index.ts deleted file mode 100644 index cda1930c614..00000000000 --- a/libs/common/src/billing/models/request/tax/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./preview-tax-amount-for-organization-trial.request"; diff --git a/libs/common/src/billing/models/request/tax/preview-tax-amount-for-organization-trial.request.ts b/libs/common/src/billing/models/request/tax/preview-tax-amount-for-organization-trial.request.ts deleted file mode 100644 index 3f366335a47..00000000000 --- a/libs/common/src/billing/models/request/tax/preview-tax-amount-for-organization-trial.request.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PlanType, ProductType } from "../../../enums"; - -export type PreviewTaxAmountForOrganizationTrialRequest = { - planType: PlanType; - productType: ProductType; - taxInformation: { - country: string; - postalCode: string; - taxId?: string; - }; -}; diff --git a/libs/common/src/billing/models/request/tokenized-payment-source.request.ts b/libs/common/src/billing/models/request/tokenized-payment-source.request.ts deleted file mode 100644 index e4bf575cc6a..00000000000 --- a/libs/common/src/billing/models/request/tokenized-payment-source.request.ts +++ /dev/null @@ -1,8 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { PaymentMethodType } from "../../enums"; - -export class TokenizedPaymentSourceRequest { - type: PaymentMethodType; - token: string; -} diff --git a/libs/common/src/billing/models/request/update-payment-method.request.ts b/libs/common/src/billing/models/request/update-payment-method.request.ts deleted file mode 100644 index 10b03103716..00000000000 --- a/libs/common/src/billing/models/request/update-payment-method.request.ts +++ /dev/null @@ -1,9 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request"; -import { TokenizedPaymentSourceRequest } from "./tokenized-payment-source.request"; - -export class UpdatePaymentMethodRequest { - paymentSource: TokenizedPaymentSourceRequest; - taxInformation: ExpandedTaxInfoUpdateRequest; -} diff --git a/libs/common/src/billing/models/request/verify-bank-account.request.ts b/libs/common/src/billing/models/request/verify-bank-account.request.ts deleted file mode 100644 index ee85d1a2aad..00000000000 --- a/libs/common/src/billing/models/request/verify-bank-account.request.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class VerifyBankAccountRequest { - descriptorCode: string; - - constructor(descriptorCode: string) { - this.descriptorCode = descriptorCode; - } -} diff --git a/libs/common/src/billing/models/response/billing-payment.response.ts b/libs/common/src/billing/models/response/billing-payment.response.ts deleted file mode 100644 index e60a11c0772..00000000000 --- a/libs/common/src/billing/models/response/billing-payment.response.ts +++ /dev/null @@ -1,17 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { BaseResponse } from "../../../models/response/base.response"; - -import { BillingSourceResponse } from "./billing.response"; - -export class BillingPaymentResponse extends BaseResponse { - balance: number; - paymentSource: BillingSourceResponse; - - constructor(response: any) { - super(response); - this.balance = this.getResponseProperty("Balance"); - const paymentSource = this.getResponseProperty("PaymentSource"); - this.paymentSource = paymentSource == null ? null : new BillingSourceResponse(paymentSource); - } -} diff --git a/libs/common/src/billing/models/response/payment-method.response.ts b/libs/common/src/billing/models/response/payment-method.response.ts deleted file mode 100644 index 34e95032aef..00000000000 --- a/libs/common/src/billing/models/response/payment-method.response.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BaseResponse } from "../../../models/response/base.response"; - -import { PaymentSourceResponse } from "./payment-source.response"; -import { TaxInfoResponse } from "./tax-info.response"; - -export class PaymentMethodResponse extends BaseResponse { - accountCredit: number; - paymentSource?: PaymentSourceResponse; - subscriptionStatus?: string; - taxInformation?: TaxInfoResponse; - - constructor(response: any) { - super(response); - this.accountCredit = this.getResponseProperty("AccountCredit"); - - const paymentSource = this.getResponseProperty("PaymentSource"); - if (paymentSource) { - this.paymentSource = new PaymentSourceResponse(paymentSource); - } - - this.subscriptionStatus = this.getResponseProperty("SubscriptionStatus"); - - const taxInformation = this.getResponseProperty("TaxInformation"); - if (taxInformation) { - this.taxInformation = new TaxInfoResponse(taxInformation); - } - } -} diff --git a/libs/common/src/billing/models/response/tax-id-types.response.ts b/libs/common/src/billing/models/response/tax-id-types.response.ts deleted file mode 100644 index f31f2133b34..00000000000 --- a/libs/common/src/billing/models/response/tax-id-types.response.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BaseResponse } from "../../../models/response/base.response"; - -export class TaxIdTypesResponse extends BaseResponse { - taxIdTypes: TaxIdTypeResponse[] = []; - - constructor(response: any) { - super(response); - const taxIdTypes = this.getResponseProperty("TaxIdTypes"); - if (taxIdTypes && taxIdTypes.length) { - this.taxIdTypes = taxIdTypes.map((t: any) => new TaxIdTypeResponse(t)); - } - } -} - -export class TaxIdTypeResponse extends BaseResponse { - code: string; - country: string; - description: string; - example: string; - - constructor(response: any) { - super(response); - this.code = this.getResponseProperty("Code"); - this.country = this.getResponseProperty("Country"); - this.description = this.getResponseProperty("Description"); - this.example = this.getResponseProperty("Example"); - } -} diff --git a/libs/common/src/billing/models/response/tax/index.ts b/libs/common/src/billing/models/response/tax/index.ts deleted file mode 100644 index 525d6d7c80a..00000000000 --- a/libs/common/src/billing/models/response/tax/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./preview-tax-amount.response"; diff --git a/libs/common/src/billing/models/response/tax/preview-tax-amount.response.ts b/libs/common/src/billing/models/response/tax/preview-tax-amount.response.ts deleted file mode 100644 index cf15156551a..00000000000 --- a/libs/common/src/billing/models/response/tax/preview-tax-amount.response.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BaseResponse } from "../../../../models/response/base.response"; - -export class PreviewTaxAmountResponse extends BaseResponse { - taxAmount: number; - - constructor(response: any) { - super(response); - - this.taxAmount = this.getResponseProperty("TaxAmount"); - } -} diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 2292f26e616..a34809e9f02 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,23 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; - import { ApiService } from "../../abstractions/api.service"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; import { ListResponse } from "../../models/response/list.response"; import { BillingApiServiceAbstraction } from "../abstractions"; -import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; -import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request"; import { SubscriptionCancellationRequest } from "../models/request/subscription-cancellation.request"; import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; -import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request"; -import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request"; import { InvoicesResponse } from "../models/response/invoices.response"; import { OrganizationBillingMetadataResponse } from "../models/response/organization-billing-metadata.response"; -import { PaymentMethodResponse } from "../models/response/payment-method.response"; import { PlanResponse } from "../models/response/plan.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; @@ -54,21 +47,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { ); } - async createSetupIntent(type: PaymentMethodType) { - const getPath = () => { - switch (type) { - case PaymentMethodType.BankAccount: { - return "/setup-intent/bank-account"; - } - case PaymentMethodType.Card: { - return "/setup-intent/card"; - } - } - }; - const response = await this.apiService.send("POST", getPath(), null, true, true); - return response as string; - } - async getOrganizationBillingMetadata( organizationId: string, ): Promise { @@ -83,17 +61,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new OrganizationBillingMetadataResponse(r); } - async getOrganizationPaymentMethod(organizationId: string): Promise { - const response = await this.apiService.send( - "GET", - "/organizations/" + organizationId + "/billing/payment-method", - null, - true, - true, - ); - return new PaymentMethodResponse(response); - } - async getPlans(): Promise> { const r = await this.apiService.send("GET", "/plans", null, false, true); return new ListResponse(r, PlanResponse); @@ -145,43 +112,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new ProviderSubscriptionResponse(response); } - async getProviderTaxInformation(providerId: string): Promise { - const response = await this.apiService.send( - "GET", - "/providers/" + providerId + "/billing/tax-information", - null, - true, - true, - ); - return new TaxInfoResponse(response); - } - - async updateOrganizationPaymentMethod( - organizationId: string, - request: UpdatePaymentMethodRequest, - ): Promise { - return await this.apiService.send( - "PUT", - "/organizations/" + organizationId + "/billing/payment-method", - request, - true, - false, - ); - } - - async updateOrganizationTaxInformation( - organizationId: string, - request: ExpandedTaxInfoUpdateRequest, - ): Promise { - return await this.apiService.send( - "PUT", - "/organizations/" + organizationId + "/billing/tax-information", - request, - true, - false, - ); - } - async updateProviderClientOrganization( providerId: string, organizationId: string, @@ -196,55 +126,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { ); } - async updateProviderPaymentMethod( - providerId: string, - request: UpdatePaymentMethodRequest, - ): Promise { - return await this.apiService.send( - "PUT", - "/providers/" + providerId + "/billing/payment-method", - request, - true, - false, - ); - } - - async updateProviderTaxInformation(providerId: string, request: ExpandedTaxInfoUpdateRequest) { - return await this.apiService.send( - "PUT", - "/providers/" + providerId + "/billing/tax-information", - request, - true, - false, - ); - } - - async verifyOrganizationBankAccount( - organizationId: string, - request: VerifyBankAccountRequest, - ): Promise { - return await this.apiService.send( - "POST", - "/organizations/" + organizationId + "/billing/payment-method/verify-bank-account", - request, - true, - false, - ); - } - - async verifyProviderBankAccount( - providerId: string, - request: VerifyBankAccountRequest, - ): Promise { - return await this.apiService.send( - "POST", - "/providers/" + providerId + "/billing/payment-method/verify-bank-account", - request, - true, - false, - ); - } - async restartSubscription( organizationId: string, request: OrganizationCreateRequest, diff --git a/libs/common/src/billing/services/organization-billing.service.spec.ts b/libs/common/src/billing/services/organization-billing.service.spec.ts index 42cfb4a5371..a14dd0f0279 100644 --- a/libs/common/src/billing/services/organization-billing.service.spec.ts +++ b/libs/common/src/billing/services/organization-billing.service.spec.ts @@ -23,7 +23,6 @@ import { OrganizationResponse } from "../../admin-console/models/response/organi import { EncString } from "../../key-management/crypto/models/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { OrgKey } from "../../types/key"; -import { PaymentMethodResponse } from "../models/response/payment-method.response"; describe("OrganizationBillingService", () => { let apiService: jest.Mocked; @@ -62,47 +61,6 @@ describe("OrganizationBillingService", () => { return jest.resetAllMocks(); }); - describe("getPaymentSource()", () => { - it("given a valid organization id, then it returns a payment source", async () => { - //Arrange - const orgId = "organization-test"; - const paymentMethodResponse = { - paymentSource: { type: PaymentMethodType.Card }, - } as PaymentMethodResponse; - billingApiService.getOrganizationPaymentMethod.mockResolvedValue(paymentMethodResponse); - - //Act - const returnedPaymentSource = await sut.getPaymentSource(orgId); - - //Assert - expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1); - expect(returnedPaymentSource).toEqual(paymentMethodResponse.paymentSource); - }); - - it("given an invalid organizationId, it should return undefined", async () => { - //Arrange - const orgId = "invalid-id"; - billingApiService.getOrganizationPaymentMethod.mockResolvedValue(null); - - //Act - const returnedPaymentSource = await sut.getPaymentSource(orgId); - - //Assert - expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1); - expect(returnedPaymentSource).toBeUndefined(); - }); - - it("given an API error occurs, then it throws the error", async () => { - // Arrange - const orgId = "error-org"; - billingApiService.getOrganizationPaymentMethod.mockRejectedValue(new Error("API Error")); - - // Act & Assert - await expect(sut.getPaymentSource(orgId)).rejects.toThrow("API Error"); - expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1); - }); - }); - describe("purchaseSubscription()", () => { it("given valid subscription information, then it returns successful response", async () => { //Arrange @@ -118,7 +76,7 @@ describe("OrganizationBillingService", () => { const organizationResponse = { name: subscriptionInformation.organization.name, billingEmail: subscriptionInformation.organization.billingEmail, - planType: subscriptionInformation.plan.type, + planType: subscriptionInformation.plan!.type, } as OrganizationResponse; organizationApiService.create.mockResolvedValue(organizationResponse); @@ -201,8 +159,8 @@ describe("OrganizationBillingService", () => { const organizationResponse = { name: subscriptionInformation.organization.name, - plan: { type: subscriptionInformation.plan.type }, - planType: subscriptionInformation.plan.type, + plan: { type: subscriptionInformation.plan!.type }, + planType: subscriptionInformation.plan!.type, } as OrganizationResponse; organizationApiService.createWithoutPayment.mockResolvedValue(organizationResponse); @@ -262,7 +220,7 @@ describe("OrganizationBillingService", () => { const organizationResponse = { name: subscriptionInformation.organization.name, billingEmail: subscriptionInformation.organization.billingEmail, - planType: subscriptionInformation.plan.type, + planType: subscriptionInformation.plan!.type, } as OrganizationResponse; organizationApiService.create.mockResolvedValue(organizationResponse); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index 53ce727df68..4120047a15f 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -25,7 +25,6 @@ import { } from "../abstractions"; import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; -import { PaymentSourceResponse } from "../models/response/payment-source.response"; interface OrganizationKeys { encryptedKey: EncString; @@ -45,11 +44,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} - async getPaymentSource(organizationId: string): Promise { - const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId); - return paymentMethod?.paymentSource; - } - async purchaseSubscription( subscription: SubscriptionInformation, activeUserId: UserId, diff --git a/libs/common/src/billing/services/tax.service.ts b/libs/common/src/billing/services/tax.service.ts deleted file mode 100644 index 27966016913..00000000000 --- a/libs/common/src/billing/services/tax.service.ts +++ /dev/null @@ -1,318 +0,0 @@ -import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax"; - -import { ApiService } from "../../abstractions/api.service"; -import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction"; -import { CountryListItem } from "../models/domain"; -import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; -import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; -import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; - -export class TaxService implements TaxServiceAbstraction { - constructor(private apiService: ApiService) {} - - getCountries(): CountryListItem[] { - return [ - { name: "-- Select --", value: "", disabled: false }, - { name: "United States", value: "US", disabled: false }, - { name: "China", value: "CN", disabled: false }, - { name: "France", value: "FR", disabled: false }, - { name: "Germany", value: "DE", disabled: false }, - { name: "Canada", value: "CA", disabled: false }, - { name: "United Kingdom", value: "GB", disabled: false }, - { name: "Australia", value: "AU", disabled: false }, - { name: "India", value: "IN", disabled: false }, - { name: "", value: "-", disabled: true }, - { name: "Afghanistan", value: "AF", disabled: false }, - { name: "Åland Islands", value: "AX", disabled: false }, - { name: "Albania", value: "AL", disabled: false }, - { name: "Algeria", value: "DZ", disabled: false }, - { name: "American Samoa", value: "AS", disabled: false }, - { name: "Andorra", value: "AD", disabled: false }, - { name: "Angola", value: "AO", disabled: false }, - { name: "Anguilla", value: "AI", disabled: false }, - { name: "Antarctica", value: "AQ", disabled: false }, - { name: "Antigua and Barbuda", value: "AG", disabled: false }, - { name: "Argentina", value: "AR", disabled: false }, - { name: "Armenia", value: "AM", disabled: false }, - { name: "Aruba", value: "AW", disabled: false }, - { name: "Austria", value: "AT", disabled: false }, - { name: "Azerbaijan", value: "AZ", disabled: false }, - { name: "Bahamas", value: "BS", disabled: false }, - { name: "Bahrain", value: "BH", disabled: false }, - { name: "Bangladesh", value: "BD", disabled: false }, - { name: "Barbados", value: "BB", disabled: false }, - { name: "Belarus", value: "BY", disabled: false }, - { name: "Belgium", value: "BE", disabled: false }, - { name: "Belize", value: "BZ", disabled: false }, - { name: "Benin", value: "BJ", disabled: false }, - { name: "Bermuda", value: "BM", disabled: false }, - { name: "Bhutan", value: "BT", disabled: false }, - { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, - { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, - { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, - { name: "Botswana", value: "BW", disabled: false }, - { name: "Bouvet Island", value: "BV", disabled: false }, - { name: "Brazil", value: "BR", disabled: false }, - { name: "British Indian Ocean Territory", value: "IO", disabled: false }, - { name: "Brunei Darussalam", value: "BN", disabled: false }, - { name: "Bulgaria", value: "BG", disabled: false }, - { name: "Burkina Faso", value: "BF", disabled: false }, - { name: "Burundi", value: "BI", disabled: false }, - { name: "Cambodia", value: "KH", disabled: false }, - { name: "Cameroon", value: "CM", disabled: false }, - { name: "Cape Verde", value: "CV", disabled: false }, - { name: "Cayman Islands", value: "KY", disabled: false }, - { name: "Central African Republic", value: "CF", disabled: false }, - { name: "Chad", value: "TD", disabled: false }, - { name: "Chile", value: "CL", disabled: false }, - { name: "Christmas Island", value: "CX", disabled: false }, - { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, - { name: "Colombia", value: "CO", disabled: false }, - { name: "Comoros", value: "KM", disabled: false }, - { name: "Congo", value: "CG", disabled: false }, - { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, - { name: "Cook Islands", value: "CK", disabled: false }, - { name: "Costa Rica", value: "CR", disabled: false }, - { name: "Côte d'Ivoire", value: "CI", disabled: false }, - { name: "Croatia", value: "HR", disabled: false }, - { name: "Cuba", value: "CU", disabled: false }, - { name: "Curaçao", value: "CW", disabled: false }, - { name: "Cyprus", value: "CY", disabled: false }, - { name: "Czech Republic", value: "CZ", disabled: false }, - { name: "Denmark", value: "DK", disabled: false }, - { name: "Djibouti", value: "DJ", disabled: false }, - { name: "Dominica", value: "DM", disabled: false }, - { name: "Dominican Republic", value: "DO", disabled: false }, - { name: "Ecuador", value: "EC", disabled: false }, - { name: "Egypt", value: "EG", disabled: false }, - { name: "El Salvador", value: "SV", disabled: false }, - { name: "Equatorial Guinea", value: "GQ", disabled: false }, - { name: "Eritrea", value: "ER", disabled: false }, - { name: "Estonia", value: "EE", disabled: false }, - { name: "Ethiopia", value: "ET", disabled: false }, - { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, - { name: "Faroe Islands", value: "FO", disabled: false }, - { name: "Fiji", value: "FJ", disabled: false }, - { name: "Finland", value: "FI", disabled: false }, - { name: "French Guiana", value: "GF", disabled: false }, - { name: "French Polynesia", value: "PF", disabled: false }, - { name: "French Southern Territories", value: "TF", disabled: false }, - { name: "Gabon", value: "GA", disabled: false }, - { name: "Gambia", value: "GM", disabled: false }, - { name: "Georgia", value: "GE", disabled: false }, - { name: "Ghana", value: "GH", disabled: false }, - { name: "Gibraltar", value: "GI", disabled: false }, - { name: "Greece", value: "GR", disabled: false }, - { name: "Greenland", value: "GL", disabled: false }, - { name: "Grenada", value: "GD", disabled: false }, - { name: "Guadeloupe", value: "GP", disabled: false }, - { name: "Guam", value: "GU", disabled: false }, - { name: "Guatemala", value: "GT", disabled: false }, - { name: "Guernsey", value: "GG", disabled: false }, - { name: "Guinea", value: "GN", disabled: false }, - { name: "Guinea-Bissau", value: "GW", disabled: false }, - { name: "Guyana", value: "GY", disabled: false }, - { name: "Haiti", value: "HT", disabled: false }, - { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, - { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, - { name: "Honduras", value: "HN", disabled: false }, - { name: "Hong Kong", value: "HK", disabled: false }, - { name: "Hungary", value: "HU", disabled: false }, - { name: "Iceland", value: "IS", disabled: false }, - { name: "Indonesia", value: "ID", disabled: false }, - { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, - { name: "Iraq", value: "IQ", disabled: false }, - { name: "Ireland", value: "IE", disabled: false }, - { name: "Isle of Man", value: "IM", disabled: false }, - { name: "Israel", value: "IL", disabled: false }, - { name: "Italy", value: "IT", disabled: false }, - { name: "Jamaica", value: "JM", disabled: false }, - { name: "Japan", value: "JP", disabled: false }, - { name: "Jersey", value: "JE", disabled: false }, - { name: "Jordan", value: "JO", disabled: false }, - { name: "Kazakhstan", value: "KZ", disabled: false }, - { name: "Kenya", value: "KE", disabled: false }, - { name: "Kiribati", value: "KI", disabled: false }, - { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, - { name: "Korea, Republic of", value: "KR", disabled: false }, - { name: "Kuwait", value: "KW", disabled: false }, - { name: "Kyrgyzstan", value: "KG", disabled: false }, - { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, - { name: "Latvia", value: "LV", disabled: false }, - { name: "Lebanon", value: "LB", disabled: false }, - { name: "Lesotho", value: "LS", disabled: false }, - { name: "Liberia", value: "LR", disabled: false }, - { name: "Libya", value: "LY", disabled: false }, - { name: "Liechtenstein", value: "LI", disabled: false }, - { name: "Lithuania", value: "LT", disabled: false }, - { name: "Luxembourg", value: "LU", disabled: false }, - { name: "Macao", value: "MO", disabled: false }, - { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, - { name: "Madagascar", value: "MG", disabled: false }, - { name: "Malawi", value: "MW", disabled: false }, - { name: "Malaysia", value: "MY", disabled: false }, - { name: "Maldives", value: "MV", disabled: false }, - { name: "Mali", value: "ML", disabled: false }, - { name: "Malta", value: "MT", disabled: false }, - { name: "Marshall Islands", value: "MH", disabled: false }, - { name: "Martinique", value: "MQ", disabled: false }, - { name: "Mauritania", value: "MR", disabled: false }, - { name: "Mauritius", value: "MU", disabled: false }, - { name: "Mayotte", value: "YT", disabled: false }, - { name: "Mexico", value: "MX", disabled: false }, - { name: "Micronesia, Federated States of", value: "FM", disabled: false }, - { name: "Moldova, Republic of", value: "MD", disabled: false }, - { name: "Monaco", value: "MC", disabled: false }, - { name: "Mongolia", value: "MN", disabled: false }, - { name: "Montenegro", value: "ME", disabled: false }, - { name: "Montserrat", value: "MS", disabled: false }, - { name: "Morocco", value: "MA", disabled: false }, - { name: "Mozambique", value: "MZ", disabled: false }, - { name: "Myanmar", value: "MM", disabled: false }, - { name: "Namibia", value: "NA", disabled: false }, - { name: "Nauru", value: "NR", disabled: false }, - { name: "Nepal", value: "NP", disabled: false }, - { name: "Netherlands", value: "NL", disabled: false }, - { name: "New Caledonia", value: "NC", disabled: false }, - { name: "New Zealand", value: "NZ", disabled: false }, - { name: "Nicaragua", value: "NI", disabled: false }, - { name: "Niger", value: "NE", disabled: false }, - { name: "Nigeria", value: "NG", disabled: false }, - { name: "Niue", value: "NU", disabled: false }, - { name: "Norfolk Island", value: "NF", disabled: false }, - { name: "Northern Mariana Islands", value: "MP", disabled: false }, - { name: "Norway", value: "NO", disabled: false }, - { name: "Oman", value: "OM", disabled: false }, - { name: "Pakistan", value: "PK", disabled: false }, - { name: "Palau", value: "PW", disabled: false }, - { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, - { name: "Panama", value: "PA", disabled: false }, - { name: "Papua New Guinea", value: "PG", disabled: false }, - { name: "Paraguay", value: "PY", disabled: false }, - { name: "Peru", value: "PE", disabled: false }, - { name: "Philippines", value: "PH", disabled: false }, - { name: "Pitcairn", value: "PN", disabled: false }, - { name: "Poland", value: "PL", disabled: false }, - { name: "Portugal", value: "PT", disabled: false }, - { name: "Puerto Rico", value: "PR", disabled: false }, - { name: "Qatar", value: "QA", disabled: false }, - { name: "Réunion", value: "RE", disabled: false }, - { name: "Romania", value: "RO", disabled: false }, - { name: "Russian Federation", value: "RU", disabled: false }, - { name: "Rwanda", value: "RW", disabled: false }, - { name: "Saint Barthélemy", value: "BL", disabled: false }, - { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, - { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, - { name: "Saint Lucia", value: "LC", disabled: false }, - { name: "Saint Martin (French part)", value: "MF", disabled: false }, - { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, - { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, - { name: "Samoa", value: "WS", disabled: false }, - { name: "San Marino", value: "SM", disabled: false }, - { name: "Sao Tome and Principe", value: "ST", disabled: false }, - { name: "Saudi Arabia", value: "SA", disabled: false }, - { name: "Senegal", value: "SN", disabled: false }, - { name: "Serbia", value: "RS", disabled: false }, - { name: "Seychelles", value: "SC", disabled: false }, - { name: "Sierra Leone", value: "SL", disabled: false }, - { name: "Singapore", value: "SG", disabled: false }, - { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, - { name: "Slovakia", value: "SK", disabled: false }, - { name: "Slovenia", value: "SI", disabled: false }, - { name: "Solomon Islands", value: "SB", disabled: false }, - { name: "Somalia", value: "SO", disabled: false }, - { name: "South Africa", value: "ZA", disabled: false }, - { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, - { name: "South Sudan", value: "SS", disabled: false }, - { name: "Spain", value: "ES", disabled: false }, - { name: "Sri Lanka", value: "LK", disabled: false }, - { name: "Sudan", value: "SD", disabled: false }, - { name: "Suriname", value: "SR", disabled: false }, - { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, - { name: "Swaziland", value: "SZ", disabled: false }, - { name: "Sweden", value: "SE", disabled: false }, - { name: "Switzerland", value: "CH", disabled: false }, - { name: "Syrian Arab Republic", value: "SY", disabled: false }, - { name: "Taiwan", value: "TW", disabled: false }, - { name: "Tajikistan", value: "TJ", disabled: false }, - { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, - { name: "Thailand", value: "TH", disabled: false }, - { name: "Timor-Leste", value: "TL", disabled: false }, - { name: "Togo", value: "TG", disabled: false }, - { name: "Tokelau", value: "TK", disabled: false }, - { name: "Tonga", value: "TO", disabled: false }, - { name: "Trinidad and Tobago", value: "TT", disabled: false }, - { name: "Tunisia", value: "TN", disabled: false }, - { name: "Turkey", value: "TR", disabled: false }, - { name: "Turkmenistan", value: "TM", disabled: false }, - { name: "Turks and Caicos Islands", value: "TC", disabled: false }, - { name: "Tuvalu", value: "TV", disabled: false }, - { name: "Uganda", value: "UG", disabled: false }, - { name: "Ukraine", value: "UA", disabled: false }, - { name: "United Arab Emirates", value: "AE", disabled: false }, - { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, - { name: "Uruguay", value: "UY", disabled: false }, - { name: "Uzbekistan", value: "UZ", disabled: false }, - { name: "Vanuatu", value: "VU", disabled: false }, - { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, - { name: "Viet Nam", value: "VN", disabled: false }, - { name: "Virgin Islands, British", value: "VG", disabled: false }, - { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, - { name: "Wallis and Futuna", value: "WF", disabled: false }, - { name: "Western Sahara", value: "EH", disabled: false }, - { name: "Yemen", value: "YE", disabled: false }, - { name: "Zambia", value: "ZM", disabled: false }, - { name: "Zimbabwe", value: "ZW", disabled: false }, - ]; - } - - async isCountrySupported(country: string): Promise { - const response = await this.apiService.send( - "GET", - "/tax/is-country-supported?country=" + country, - null, - true, - true, - ); - return response; - } - - async previewIndividualInvoice( - request: PreviewIndividualInvoiceRequest, - ): Promise { - const response = await this.apiService.send( - "POST", - "/accounts/billing/preview-invoice", - request, - true, - true, - ); - return new PreviewInvoiceResponse(response); - } - - async previewOrganizationInvoice( - request: PreviewOrganizationInvoiceRequest, - ): Promise { - const response = await this.apiService.send( - "POST", - `/invoices/preview-organization`, - request, - true, - true, - ); - return new PreviewInvoiceResponse(response); - } - - async previewTaxAmountForOrganizationTrial( - request: PreviewTaxAmountForOrganizationTrialRequest, - ): Promise { - const response = await this.apiService.send( - "POST", - "/tax/preview-amount/organization-trial", - request, - true, - true, - ); - return response as number; - } -} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 67836befd7c..578d09c9aea 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -23,7 +23,6 @@ export enum FeatureFlag { /* Billing */ TrialPaymentOptional = "PM-8163-trial-payment", PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", - PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout", PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover", PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings", @@ -100,7 +99,6 @@ export const DefaultFeatureFlagValue = { /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, - [FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE, [FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE, [FeatureFlag.PM22415_TaxIDWarnings]: FALSE, diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 70ba76fe797..b10df69e277 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -90,14 +90,10 @@ import { } from "../auth/models/response/two-factor-web-authn.response"; import { TwoFactorYubiKeyResponse } from "../auth/models/response/two-factor-yubi-key.response"; import { BitPayInvoiceRequest } from "../billing/models/request/bit-pay-invoice.request"; -import { PaymentRequest } from "../billing/models/request/payment.request"; -import { TaxInfoUpdateRequest } from "../billing/models/request/tax-info-update.request"; import { BillingHistoryResponse } from "../billing/models/response/billing-history.response"; -import { BillingPaymentResponse } from "../billing/models/response/billing-payment.response"; import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; -import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; import { ClientType, DeviceType } from "../enums"; import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request"; import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request"; @@ -294,11 +290,6 @@ export class ApiService implements ApiServiceAbstraction { return new SubscriptionResponse(r); } - async getTaxInfo(): Promise { - const r = await this.send("GET", "/accounts/tax", null, true, true); - return new TaxInfoResponse(r); - } - async putProfile(request: UpdateProfileRequest): Promise { const r = await this.send("PUT", "/accounts/profile", request, true, true); return new ProfileResponse(r); @@ -309,10 +300,6 @@ export class ApiService implements ApiServiceAbstraction { return new ProfileResponse(r); } - putTaxInfo(request: TaxInfoUpdateRequest): Promise { - return this.send("PUT", "/accounts/tax", request, true, false); - } - async postPrelogin(request: PreloginRequest): Promise { const env = await firstValueFrom(this.environmentService.environment$); const r = await this.send( @@ -365,10 +352,6 @@ export class ApiService implements ApiServiceAbstraction { return new PaymentResponse(r); } - postAccountPayment(request: PaymentRequest): Promise { - return this.send("POST", "/accounts/payment", request, true, false); - } - postAccountLicense(data: FormData): Promise { return this.send("POST", "/accounts/license", data, true, false); } @@ -429,11 +412,6 @@ export class ApiService implements ApiServiceAbstraction { return new BillingHistoryResponse(r); } - async getUserBillingPayment(): Promise { - const r = await this.send("GET", "/accounts/billing/payment-method", null, true, true); - return new BillingPaymentResponse(r); - } - // Cipher APIs async getCipher(id: string): Promise { From 7b94d6ab2b35ba65f294454daa6de7901be81015 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:28:47 -0500 Subject: [PATCH 54/55] [PM-18717] Fix multiple organization situation for Free Families Policy sponsorship (#16611) * Resolve multiple org situation * Fix multi org policy mismatch issue --- .../services/free-families-policy.service.ts | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts index 7a8e3804b2c..52041936e50 100644 --- a/apps/web/src/app/billing/services/free-families-policy.service.ts +++ b/apps/web/src/app/billing/services/free-families-policy.service.ts @@ -19,12 +19,6 @@ interface EnterpriseOrgStatus { @Injectable({ providedIn: "root" }) export class FreeFamiliesPolicyService { - protected enterpriseOrgStatus: EnterpriseOrgStatus = { - isFreeFamilyPolicyEnabled: false, - belongToOneEnterpriseOrgs: false, - belongToMultipleEnterpriseOrgs: false, - }; - constructor( private policyService: PolicyService, private organizationService: OrganizationService, @@ -104,9 +98,11 @@ export class FreeFamiliesPolicyService { if (!orgStatus) { return false; } - const { belongToOneEnterpriseOrgs, isFreeFamilyPolicyEnabled } = orgStatus; + const { isFreeFamilyPolicyEnabled } = orgStatus; const hasSponsorshipOrgs = organizations.some((org) => org.canManageSponsorships); - return hasSponsorshipOrgs && !(belongToOneEnterpriseOrgs && isFreeFamilyPolicyEnabled); + + // Hide if ANY organization has the policy enabled + return hasSponsorshipOrgs && !isFreeFamilyPolicyEnabled; } checkEnterpriseOrganizationsAndFetchPolicy(): Observable { @@ -122,16 +118,12 @@ export class FreeFamiliesPolicyService { const { belongToOneEnterpriseOrgs, belongToMultipleEnterpriseOrgs } = this.evaluateEnterpriseOrganizations(organizations); - if (!belongToOneEnterpriseOrgs) { - return of({ - isFreeFamilyPolicyEnabled: false, - belongToOneEnterpriseOrgs, - belongToMultipleEnterpriseOrgs, - }); - } + // Get all enterprise organization IDs + const enterpriseOrgIds = organizations + .filter((org) => org.canManageSponsorships) + .map((org) => org.id); - const organizationId = this.getOrganizationIdForOneEnterprise(organizations); - if (!organizationId) { + if (enterpriseOrgIds.length === 0) { return of({ isFreeFamilyPolicyEnabled: false, belongToOneEnterpriseOrgs, @@ -145,8 +137,8 @@ export class FreeFamiliesPolicyService { this.policyService.policiesByType$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), ), map((policies) => ({ - isFreeFamilyPolicyEnabled: policies.some( - (policy) => policy.organizationId === organizationId && policy.enabled, + isFreeFamilyPolicyEnabled: enterpriseOrgIds.every((orgId) => + policies.some((policy) => policy.organizationId === orgId && policy.enabled), ), belongToOneEnterpriseOrgs, belongToMultipleEnterpriseOrgs, @@ -166,9 +158,4 @@ export class FreeFamiliesPolicyService { belongToMultipleEnterpriseOrgs: count > 1, }; } - - private getOrganizationIdForOneEnterprise(organizations: any[]): string | null { - const enterpriseOrganizations = organizations.filter((org) => org.canManageSponsorships); - return enterpriseOrganizations.length === 1 ? enterpriseOrganizations[0].id : null; - } } From de3759fa85cfb1010d80118e565a8e55b2532188 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:54:33 -0700 Subject: [PATCH 55/55] refactor(sso-config-tweaks): [Auth/PM-933] Tweaks to SSO Config Page (#16374) Makes some tweaks to the SSO config page: - SSO Identifier: update hint text - Single Sign-On Service URL: make required, remove hint text - Client Secret: make hidden by default (add view/hide toggle) --- apps/web/src/locales/en/messages.json | 10 +++++++--- .../bit-web/src/app/auth/sso/sso.component.html | 14 ++++++++++---- .../bit-web/src/app/auth/sso/sso.component.ts | 4 +++- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 4bb4c8873ee..35f369aa647 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5219,9 +5219,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index ef8241b534c..6d2836ee0ba 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -30,8 +30,8 @@ {{ "ssoIdentifier" | i18n }} - {{ "ssoIdentifierHintPartOne" | i18n }} - {{ "claimedDomains" | i18n }} + {{ "ssoIdentifierHint" | i18n }} + {{ "claimedDomainsLearnMore" | i18n }} @@ -209,7 +209,14 @@ {{ "clientSecret" | i18n }} - + + @@ -488,7 +495,6 @@ formControlName="idpSingleSignOnServiceUrl" appInputStripSpaces /> - {{ "idpSingleSignOnServiceUrlRequired" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 9baeaabb33f..f68e35bf240 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -121,6 +121,8 @@ export class SsoComponent implements OnInit, OnDestroy { spMetadataUrl: string; spAcsUrl: string; + showClientSecret = false; + protected openIdForm = this.formBuilder.group>( { authority: new FormControl("", Validators.required), @@ -156,7 +158,7 @@ export class SsoComponent implements OnInit, OnDestroy { idpEntityId: new FormControl("", Validators.required), idpBindingType: new FormControl(Saml2BindingType.HttpRedirect), - idpSingleSignOnServiceUrl: new FormControl(), + idpSingleSignOnServiceUrl: new FormControl("", Validators.required), idpSingleLogoutServiceUrl: new FormControl(), idpX509PublicCert: new FormControl("", Validators.required), idpOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm),